10
Breve introducción a SUnit Objetos II – 29/05/2007 Diego Torres / Andrés Fortier [email protected] [email protected] ¿Qué es SUnit? Generalmente para probar las clases que modelamos en nuestro sistema, solemos escribir una serie de pruebas en el Workspace e inspeccionamos los resultados para asegurarnos que el sistema responde como es deseado. Este enfoque de testing presenta algunos problemas: por un lado el código suele ir cambiando a medida que vamos testeando distintas funcionalidades del modelo y sólo queda registro del test final. Asimismo no contamos con herramientas de soporte ni con la posibilidad de compartir y organizar los tests. Para ayudarnos en esta tarea, se creo un pequeño y potente framework llamado SUnit (http://sunit.sourceforge.net/ ) utilizado para el desarrollo de tests, permitiendo ordenar los tests por clases y mensajes. Tests La primera pregunta que nos podemos hacer es ¿qué es un test? y ¿para qué lo necesitamos?. Idealmente un test nos permitiría confirmar que nuestro código se comporta como queremos en cualquier contexto de ejecución; esto es algo muy difícil de lograr, ya que resulta casi imposible pensar en todos los posibles contextos de ejecución de un método, pero podemos, al menos, abarcar una serie de situaciones que son las que esperamos ocurran con más frecuencia. Muchas veces los desarrolladores o programadores piensan en los tests como una pérdida de tiempo, cosa que se encuentra muy lejos de la realidad: un test es un inversión, que a lo largo del desarrollo de un proyecto paga con creces. En el caso de llevar la noción de test a los objetos, lo que nos interesa es asegurarnos que una instancia de la clase que estamos modelando se comporte de forma adecuada. Notar que NO estamos interesados en como implementa una clase un determinado mensaje, sino cual es el comportamiento que me brinda un determinado objeto al enviarle un mensaje. Veamos un ejemplo de un test hecho a mano: s:=Set new. s add: 1. s size = 1 “Esto debería ser verdadero” En este caso estamos verificando el correcto funcionamiento del mensaje #add: (asumiendo que el mensaje #size funciona correctamente). Este test modela la siguiente propiedad: “Si a un conjunto vacío le agrego un elemento, el tamaño del conjunto luego de agregar el elemento es 1”. Notar que no nos interesa de que forma se implemente el mensaje #add:, lo que nos interesa es que la semántica asociada a ese mensaje sea correcta. De forma similar podemos plantear otra serie de tests: “Un elemento es agregado en forma correcta” s:=Set new. s add: 25. s includes: 25 “Esto debería ser verdadero”

2011oo2tp02_Introduccion+SUnit

  • Upload
    anitaaa

  • View
    213

  • Download
    0

Embed Size (px)

Citation preview

Breve introducción a SUnitObjetos II – 29/05/2007

Diego Torres / Andrés Fortier

[email protected]

[email protected]

¿Qué es SUnit?

Generalmente para probar las clases que modelamos en nuestro sistema, solemos escribir una serie de pruebas en el Workspace e inspeccionamos los resultados para asegurarnos que el sistema responde como es deseado. Este enfoque de testing presenta algunos problemas: por un lado el código suele ir cambiando a medida que vamos testeando distintas funcionalidades del modelo y sólo queda registro del test final. Asimismo no contamos con herramientas de soporte ni con la posibilidad de compartir y organizar los tests. Para ayudarnos en esta tarea, se creo un pequeño y potente framework llamado SUnit (http://sunit.sourceforge.net/) utilizado para el desarrollo de tests, permitiendo ordenar los tests por clases y mensajes.

Tests

La primera pregunta que nos podemos hacer es ¿qué es un test? y ¿para qué lo necesitamos?. Idealmente un test nos permitiría confirmar que nuestro código se comporta como queremos en cualquier contexto de ejecución; esto es algo muy difícil de lograr, ya que resulta casi imposible pensar en todos los posibles contextos de ejecución de un método, pero podemos, al menos, abarcar una serie de situaciones que son las que esperamos ocurran con más frecuencia.Muchas veces los desarrolladores o programadores piensan en los tests como una pérdida de tiempo, cosa que se encuentra muy lejos de la realidad: un test es un inversión, que a lo largo del desarrollo de un proyecto paga con creces.

En el caso de llevar la noción de test a los objetos, lo que nos interesa es asegurarnos que una instancia de la clase que estamos modelando se comporte de forma adecuada. Notar que NO estamos interesados en como implementa una clase un determinado mensaje, sino cual es el comportamiento que me brinda un determinado objeto al enviarle un mensaje.

Veamos un ejemplo de un test hecho a mano:

s:=Set new.s add: 1.s size = 1 “Esto debería ser verdadero”

En este caso estamos verificando el correcto funcionamiento del mensaje #add: (asumiendo que el mensaje #size funciona correctamente). Este test modela la siguiente propiedad: “Si a un conjunto vacío le agrego un elemento, el tamaño del conjunto luego de agregar el elemento es 1”. Notar que no nos interesa de que forma se implemente el mensaje #add:, lo que nos interesa es que la semántica asociada a ese mensaje sea correcta.

De forma similar podemos plantear otra serie de tests:

“Un elemento es agregado en forma correcta”

s:=Set new.s add: 25.s includes: 25 “Esto debería ser verdadero”

“Agregar un elemento que ya existe a un conjunto no modifica su tamaño”

s:=Set new.s add: 1.s add: 2.s size = 2 “Esto debería ser verdadero”s add: 1.s size = 2 “Esto debería ser verdadero”

Por último es necesario remarcar la importancia de tener los tests en algún lugar en forma ordenada; en la gran mayoría de los proyectos es muy común que con el pasar del tiempo se modifiquen las implementaciones de los métodos o se refactoricen las clases (creando nuevas clases o eliminando clases obsoletas) . Dado que en este proceso existe la posibilidad de introducir bugs, resulta muy útil tener a mano una serie de tests que indiquen si, a pesar de haber modificado la implementación de alguna clase, el comportamiento sigue siendo el deseado.

SUnit

Creando Tests

A la hora de plantear los tests lo mas común es generar un test-case por clase, el cual contendrá un test por cada mensaje (o conjunto de mensajes) de la clase que estemos probando. Para ponerlo de otro modo, un test-case es un conjunto de tests; generalmente se realiza un test-case por cada clase del modelo. Dentro de cada test-case, la idea es contar un test individual por cada mensaje de la clase que estamos testeando.

Generalmente un test se compone por una etapa de inicialización (donde se crea el contexto de ejecución y los casos de ejemplo a testear), el envío del mensaje que queremos probar y una etapa de testeo (donde se verifica que el funcionamiento del método haya sido correcto).

Veamos, utilizando como ejemplo la clase FilteredSet, la cual se comporta como un Set con la particularidad que agrega solamente elementos que responden a determina condición.

Luego, debemos:

1) Crear un test-case: Para esto debemos crear una subclase de TestCase.

TestCase subclass: #FilteredSetTestCaseinstanceVariableNames: 'conjunto'classVariableNames: ''poolDictionaries: ''category: 'FilteredSet'

2) Definir el código de inicialización: El código de inicialización es un mensaje que será invocado antes de ejecutarse cada uno de los tests pertenecientes al test-case. El código de inicialización se escribe en el método #setUp:

FilteredSetTestCase>>setUp

conjunto:=FilteredSet filterBlock:[:x | x > 3].

3) Definir un test por cada mensaje que deseamos verificar: para esto debemos definir un mensaje en la clase FilteredSetTestCase por cada test. Por ejemplo, si queremos verificar que los mensajes #add: y #size de la clase FilteredSet funcionen correctamente, definiremos dos mensajes en la clase FilteredSetTestCase: #testAdd y #testSize respectivamente. Notar que los mensajes deben comenzar con 'test' para ser ejecutados en forma automática.

4) Veamos un ejemplo que testee el funcionamiento del mensaje #add: (recordar que antes de evaluarse el mensaje #testAdd se llamará al mensaje #setUp, por lo que en la v.i. conjunto habrá un FilteredSet vacío)

FilteredSetTestCase>>testAdd

self conjunto add:2. “No debería agregarlo” (1)self conjunto add:4. “Debería agregarlo” (2)self assert: (self conjunto includes: 4). (3)self deny: (self conjunto includes: 2). (4)

En las líneas 1) y 2) lo que hacemos es intentar agregar los valores 2 y 4 al conjunto. Notar que dado que la condición del conjunto es que el número sea mayor a 3, el 2 no debería agregarse al conjunto.En la línea 3) verificamos que el conjunto incluya el número 4; esta verificación se hace por medio del mensaje #assert:. Este mensaje espera como parámetro un booleano y el comportamiento esperado es que ese booleano sea true; en el caso de no serlo, cuando se corra el test se indicará al programador que el test ha fallado (una aserción no ha sido cumplida). Notar que este mensaje es heredado de la superclase TestCase.En la línea 4) verificamos que el conjunto no incluya el número 2; esta verificación se hace por medio del mensaje #deny:. Este mensaje espera como parámetro un booleano y el comportamiento esperado es que, a la inversa del mensaje #assert:, ese booleano sea false; en el caso de no serlo, cuando se corra el test se indicará al programador que el test ha fallado (una negación no ha sido cumplida). Este mensaje también es heredado de la superclase.

Veamos ahora como sería el test para verificar que el mensaje #size funciona correctamente:

FilteredSetTestCase>>testSize

self assert: (conjunto size = 0). (1)conjunto add:2. “No debería agregarlo” (2)self assert: (conjunto size = 0). (3)conjunto add:4. “Debería agregarlo” (4)self assert: (conjunto size = 1). (5)conjunto add:4. “Debería agregarlo” (6)self assert: (conjunto size = 1). (7)

Recordemos nuevamente que antes de enviarse el mensaje #testSize se evalúa el mensaje #setUp, luego en la v.i. conjunto tendremos un FilteredSet vacío. En la línea 1) chequeamos que el tamaño de un conjunto vacío sea 0. En las líneas 2) y 3) chequeamos que, como el 2 no debería ser agregado al conjunto, su tamaño no debería cambiar. En 4) y 5) vemos que al agregar un elemento que cumple la condición del bloque el tamaño del conjunto se incrementa en 1. Por último, en 6) y 7) corroboramos que al agregar un elemento que cumple la condición, pero que ya se encuentra en el conjunto, su tamaño no cambia.

Notar que con estos dos tests no aseguramos que nuestra implementación del FilteredSet funcione bien en todos los contextos. Por ejemplo, deberíamos ver que sucede al agregar mas de tres elementos al conjunto (¿Porqué?)

5) Definimos la clase FilteredSet como sigue:Set subclass: #FilteredSetinstanceVariableNames: 'filterBlock'classVariableNames: ''poolDictionaries: ''category: 'FilteredSet'

y la implementación del mensaje #add:

FilteredSet>>add: newObject

(self filterBlock value: newObject) ifTrue:[super add: newObject].

6) Correr los tests: el paso final es seleccionar un test case y ejecutarlo. Ejecutar un test case implica que se evaluarán todos los mensajes de la forma 'test....' de la clase del test-case. Para esto lo primero es abrir el TestRunner evaluando la expresión “TestRunner open”. Esto abrirá la ventana que se muestra en la figura; dentro del comobo-box aparecen listadas todas las clases que representan un test-case, de la cual seleccionaremos el test case que queremos evaluar:

Una vez seleccionado el test, presionando el botón Run se evaluarán todos los tests del test case. En el caso que no haya ningún error, al finalizar la ejecución se mostrará la ventana de color verde similar a la de la figura:

Existe la posibilidad que al ejecutar un test-case se produzca algún tipo de error. Estos se pueden dividir en dos clases:

A) Una aserción que no se haya cumplido. Por ejemplo, tomemos el caso de haber implementado mal el mensaje #add: del FilteredSet:

FilteredSet>>add: newObject

(self filterBlock value: newObject) ifFalse:[super add: newObject].

En este caso, al intentar ejecutar los tests obtendremos un error, ya que la implementación no respectará la especificación:

Notar que en la ventana se especifica que se ejecutaron 2 tests (2 run), ninguno de ellos fue exitoso en términos de aserciones y negaciones (0 passed), 2 fallaron (2 failed) y no hubo otro tipo de errores (0 errors).

En el combo-box inferior aparecen los mensajes de los tests que fallaron:

Seleccionando uno de los tests se muestra la excepción que causó el problema:

En este caso podemos inspeccionar que fue lo que falló (utilizando el debugger). En este caso veremos que, en el mensaje #testAdd, ha fallado la primera aserción:

Teniendo esta información es claro que el mensaje #add: no funciona como es debido. Podemos ahora utilizar el browser de clases para cambiar la implementación del mensaje #add: para que funcione correctamente y volver a correr el test-case.

B) Errores de programación: Esto implica algún tipo de error en la lógica de algún método. Supongamos que definimos en forma incorrecta el mensaje #add: en FilteredSet de la siguiente manera:

FilteredSet>>add: newObject

(self condition value) ifTrue:[super add: newObject].

“La definición correcta es value: newObject”

En este caso, al correr el test obtendremos una pantalla de la forma:

Notar que en este caso no se indican fallas (0 failed), sino errores. Esto implica que el problema no está en alguna aserción no cumplida, sino en la codificación de algún método.

En el combo-box inferior aparecen los mensajes de los tests que fallaron:

Seleccionando uno de los tests se muestra la excepción que causó el problema:

Acto seguido podemos abrir el debugger, corregir el problema y ejecutar nuevamente el test case:

Extensión para el refactoring browser Esta extensión permite ejecutar los test directamente desde el browser de clases de VW. InstalaciónPara instalar este plug-in deberán bajarlo desde el repositorio de VW. Una forma de bajarlo es la

siguiente:En la ventana principal de su VW encontrarán, abajo a la derecha, el logo de VW con la inscripción no conectado. Haciendo click allí aparecerá la opción de conectarse al repositorio publico de Cincom (para ello necesitan una conexión a Internet). Una vez conectados, mediante el boton derecho abren el repositorio con la opción “Open Repository Manager”. Seleccionan el repositorio Cincom Public Repository en la nueva ventana y con el boton derecho abren el menú contextual y elijen published items.En la ventana de published Items buscan RBSunitExtensions y seleccionan la opción superior como muestra la figura:

Una vez seleccionada la primer opción, abrimos un menú contextual con el botón derecho del mouse y seleccionamos update. Así se instalará este paquete en nuestro VW.

Al instalarse se agregarán botones en el parte inferior del refactoring browser, que permiten correr los tests más fácilmente mientras se codifica.