Jun 23

Test Driven Development by example, de Kent Beck.

 

Aprovechando mi puente de cuatro días (fiestas locales de la ciudad en la que trabajo), me he leído "Test Driven Development by example", de Kent Beck. El libro se lee rápido, ya que hay poco texto en cada hoja, mucho código de ejemplo, mucha hoja en blanco para que pongas tus anotaciones y, sobre todo, porque al estar en inglés, me he saltado alegremente todo lo que no he entendido ;-)

De todos son conocidos los tres famosos pasos que hay que seguir con TDD

  1. Escribir un test automático de prueba y ejecutarlo para ver que falla.
  2. Hacer el código mínimo necesario para que el test pase.
  3. refactorizar el código.

Sin embargo, es necesaria la lectura de un libro como este (o aprender de alguien con experiencia) para poder entender qué significan exactamente estos pasos y cómo seguirlos de forma eficiente. Paso a continuación a detallar algunas de las conclusiones prácticas a las que te lleva la lectura del libro.

¿Cuánto debemos avanzar cada vez?

Siguiendo los tres pasos anteriores, podemos irnos a dos extremos. Por un lado, podemos hacer un test tonto, que nos lleve un minuto hacerlo. Luego podemos hacer el código para que pase ese test, que nos lleva otros dos minutos, y luego el refactor, en otros dos minutos. Al final del día podemos haber escrito doscientos tests y docientos trozitos de código y apenas haber avanzado en nuestro proyecto. El otro extremo es pasarnos dos días haciendo un test, una semana para el código de ese test y otra semana para hacer el refactor.

Según el libro, ni lo uno ni lo otro, hay que llegar al punto justo. ¿Y cual es ese punto?. Somos nosotros los que debemos decidirlo en función de nuestras "sensaciones" al ir programando. Los test y el código que hagamos no nos deben resultar demasiado triviales de forma que nos de la sensación de estar perdiendo el tiempo. Sin embargo, si deben ser del tamaño justo para que no nos cueste demasiado pensar el código que tenemos que hacer. Debemos ser capaces de hacer con cierta facilidad el código necesario para el test, sin que nos lleve más de, digamos, media hora conseguir que el test pase.

Si el código que hacemos para cada test nos resulta demasiado trivial, podemos hacer test un poco más grandes. Si el código para el test que estamos haciendo nos empieza a dar que pensar, no estamos muy seguros de cómo hacerlo o si va a funcionar a la primera, debemos hacer test más pequeños. Por ejemplo, si hacemos un test para un método suma, podemos implementar fácilmente el método. Si hacemos un test para un método recursivo de factorial, los más experimentados o con mejor cabeza para la programación pueden hacerlo directamente, pero los más novatos quizás necesiten hacer primero un test para el caso trivial e implementarlo, luego un test para otro número e implementarlo, etc.

De alguna forma, el libro da a entender que tenemos que tender más al ensayo y error con los test que pasarnos ratos largos pensando cómo implementar algo. Una buena medida de que debemos empezar a hacer test más pequeños y menos código en cada paso es cuando los test empiezan a sorprendernos con fallos inesperados, es decir, cuando ya no estamos realmente controlando la situación.

Centrarse en un test cada vez y hacer sólo lo necesario para este test

Este es quizás uno de los puntos que más fácilmente nos podemos saltar. Si hacemos un test y nos ponemos a hacer un código, es muy fácil que nos salgan situaciones o código auxiliar que necesitemos y nos pongamos a hacerlo en condiciones.

Por ejemplo, imagina que para pasar un test que requiere buscar un Alumno en una lista, vemos que necesitamos implementar el método equals() en la clase Alumno, que ese método equals() requiere un código algo rebuscado y además deberíamos implementar el método hashCode() que nos aconseja java siempre que implementemos el equals(). Es muy fácil que nos desviemos de nuestro test para hacer ese equals() y ese hashCode() lo más completos posibles y nos olvidemos temporalmente del test que nos ocupa. O quizás, haciendo el código usemos un método ya hecho y descubramos que ese método necesita un arreglo porque hay un caso que no contempla o debería hacer algo más. Es fácil que ahora nos pongamos a arreglar ese método.

Según el libro, no debemos hacer eso. Debemos implementar únicamente un equals() y/o hashCode() mínimo que nos permita pasar el test lo antes posible, incluso aunque devuelva directamente true o false y un hashCode cero, si con eso basta para que pase el test. Una vez que pasa nuestro test, podemos dar la siguiente iteración y hacer un test para el método equals() y entonces hacer una implementación correcta de equals().

Y la mejor forma de hacer esto y que no se nos olvide después y que nos sintamos cómodos haciendo esas "chapuzas" temporales, es tener un papel al lado del teclado en el que apuntemos los test que creemos que debemos  hacer más adelante. Si tenemos que hacer un equals() en condiciones, apuntamos en el papel que tenemos que hacer un test de equals() de la clase Alumno, escribimos ahora un equals() con la implementación mínima necesaria para que funcione el test en el que estamos trabajando, y nos olvidamos del equals() hasta que le toque el turno. Si tenemos que arreglar aquel método que hemos descubierto, pues apuntamos hacer un test para ese método y no lo tocamos ahora, nos centramos en el test que estamos actualmente trabajando.

Dejar que TDD nos vaya llevando al diseño más simple

Según TDD, debemos hacer en cada momento el código más simple posible que haga pasar el test. En el refactor, según el libro, debemos sobre todo trata de eliminar repeticiones (DRY, Dont repeat yourself o "no te repitas", para los amigos) y es precisamente en este paso de refactor, donde debemos "complicar" nuestro diseño sólo lo justo para evitar esas repeticiones. Veamos esto con un ejemplo concreto.

Imagínate que uno de nuestros test dice que al jefe le podemos fijar el sueldo. Hacemos un test que a la clase Jefe le pone un setSueldo() y comprueba que getSueldo() devuelve el sueldo que hemos pasado. (sí, ya sé que es muy tonto y que este tipo de cosas ni siquiera merecen la pena ser testeadas). Bueno, hacemos nuestra clase Jefe y ese par de métodos tontos y el test pasa.

Ahora, el siguiente test nos dice que podemos hacer lo mismo con un currito. Hacemos el test, hacemos la clase Currito y le ponemos los dos métodos de marras. Los test pasan, pero ahora toca refactorizar. ¿Código repetido?. Sí, las dos clases enteras, lo único que cambia es el nombre. Tal cual tenemos ahora, deberíamos hacer una clase Empleado con todo el código de setSueldo() y getSueldo() y BORRAR las clases Jefe y Currito. Sí, borrar, no hacer herencia, ni interface común ni nada parecido. Ahora mismo no hay nada que distinga a Jefe de Currito y el diseño más simple es tener una única clase con el código común (todo el código) para ambos tipos de personajes. No debemos dejarnos llevar en ningún momento por nuestro entusiasmo ni nuestros bastos conocimientos de OO para mantener la clase Jefe y Currito y hacerles una clase padre Empleado. De momento, simplemente no es necesario y por tanto, no lo hacemos.

Supón ahora que un test nos pide que escribamos en pantalla el tipo de personaje que es. Si es jefe o currito. Pues bien, la solución más simple no es volver a hacer las clases Jefe y Currito. La OO nos lo pide a gritos, pero no es la solución más simple para los tres test que tenemos. La solución más simple es hacer un enumerado JEFE, CURRITO y ponerle un atributo a la clase Empleado, que puede rellenarse en el mismo constructor, con el método getTipoEmpleado() correspondiente.

Ahora, otro test hace que alguna cosa, por ejemplo, comprobar si el Empleado tiene derecho a coche de empresa, y cómo no, sólo si es jefe tiene derecho a ello. Pues bien, mientras sólo este este test, la solución más simple es poner un if comprobando en el atributo enumerado si es jefe. El método tieneCocheEmpresa() de empleado simplemente devuelve el restultado del if.

Y finalmente, piensa en otro test que también da un privilegio al jefe, como comprobar si Empleado tiene derecho a un sillón cómodo y nuevamente sólo si es jefe tiene ese derecho. La solución más simple que podemos dar en este momento es añadir otro método tieneSillonComodo() otro if del atributo … pero cuando llega el momento de refactor vemos que hay código repetido. Ese if está en dos sitios distintos. Ahora, y sólo ahora, es el momento donde TDD nos aconseja que empezemos a pensar en la existencia de dos clases separadas, la de Jefe y la de Currito, cuando empezamos a ver que el "si es jefe" empieza a repetirse en varios sitios. Y sólo si las nuevas clases Jefe y Currito tienen código repetido, debemos mantener la clase Empleado y  heredar de ella. Si dejara de existir ese código común, simplemente eliminamos Empleado y no hacemos herencia. O quizás, en este caso concreto, sea más fácil mantener una tabla de booleanos/privilegio dentro de la clase Empleado.

De hecho, uno de los ejemplos del libro es precisamente algo parecido a esto. Comienza haciendo dos clases (Dolar y Franco) y acaba descartándolas para hacer una única clase (Dinero) que tiene un atributo que indica el tipo de moneda.

Descansos frecuentes

Y el consejo que más me ha gustado del libro: Tener siempre una botella de agua a mano, de esta forma, la fisiología te obligará a tomarte descansos frecuentes … para ir al baño.

May 29

Jugando con TDD

 

Hace tiempo que había oido hablar de TDD y hace unos meses que lo poco que codifico intento hacerlo siguiendo esta filosofía, de momento yo solo, por mi cuenta y riesgo. No me gusta mover a la gente y aconsejarles algo sin haber hecho primero mis pruebas. Pero la verdad es que en ese poco tiempo que he estado probando, las ventajas saltan a la vista. Ahí van algunas cosas que he notado.

La primera es que al pensar primero en hacer el test, te obliga un poco a hacer la/s clases/s con un diseño adecuado para ser testeadas. El ejemplo más típico es una clase que recoge datos de algún sitio, hace cosas con ellos y los deja  en otro lado. El diseño para hacer test de esa clase hasta cierto punto obliga a que esa clase se le pasen los datos (o los recoja de una interface que se le pasa) y devuelva los resultados (o se los pase a una interface que se le pasa). Este diseño, prácticamente obligado para poder hacer el test, generalmente es mejor que la clase "mazacote" que ella sola lee sus datos directamente de dónde sea, hace las cuentas y deja los resultados directamente en algún sitio. La clase "mazacote" sería muy difícil de testear, puesto que tendríamos que acceder directamente a los datos de donde la clase los recoge o los deja (una base de datos, una interface gráfica de usuario, un socket, etc). Hacer el test nos ha obligado a, en vez de hacer una clase "mazacote", hacer tres clases: LeerDatos, EcharCuentas y MostrarResultados, con una clara divisíón de responsabilidades, y además, con sus interfaces correspondientes.

La segunda ventaja, según vas haciendo test y codificando más y más, es que me siento con mucha confianza para retocar el código. El tener unos test detrás me hace ser más atrevido a la hora de tocar código que funciona para mejorarlo. De hecho, lo hago más que antes (aunque para decir la verdad, no lo hago todo lo que debiera por las malditas prisas).

Y otra ventaja verdaderamente importante y que me ha llevado a escribir este post, aunque no esté hablando estrictamente de TDD, es que ayuda muchísimo a buscar y corregir fallos. Cuento la experiencia de hoy:

En uno de nuestros sistemas enviamos mensajes por sockets entre los distintos ejecutables. Había un mensaje concreto que al enviarlo por socket aparentemente funcionaba, pero soltaba por el log unas excepciones muy feas y raras. Esas excepciones saltan en el código que convierte ese mensaje (una clase java) en un array de bytes para envío por el socket. Me "encasquetaron" a mí el problema, así que me puse a ello. Pero en vez de hacer lo que se hace tradicionalmente, que es arrancar el sistema con debugger y ponerse a probar, decidí aplicar alguna de las ideas de TDD: "No se toca código si no hay un test que falla". Así que en vez de eso, me fui a mi sitio y me puse a hacer un test que fuera capaz de provocar ese fallo.

Tras una hora y pico de trabajo y hacer un test relativamente complejo, como abrir un socket cliente y otro servidor, construir el mensaje y enviarlo y jugar con los datos del mensaje a ver qué podía provocar el error, conseguí reproducirlo. Ahora, la parte de debugger sobre el test de junit es mucho más sencilla, que es un programa muchísimo más simple que el sistema completo. Sentado tranquilamente en mi sitio con mi eclipse. Otra horita de debugger y localicé y corregí el error. Generar una nueva versión del sitema y prueba con éxito.

Pues bien, me costó casi tres horas localizar y corregir el error. No quiero ni pensar lo que habría tardado si me hubiera dedicado a arrancar el sistema completo una y otra vez, generando, instalando versiones y rearrancando cada vez que hubiera tocado algo para probar, manejando toda la interface gráfica (loggeo en el sistema, creación de unos datos, envío por el socket) en cada prueba. Estoy seguro que habría echado un día completo o más, ocupando el sistema e impidiendo a otros trabajar.

Y lo mejor de todo no es que me haya ahorrado varias horas de mi trabajo dejando libre el sistema a los probadores, no. Lo mejor de todo es que ahora hay una prueba que se ejecutará automáticamente cada vez que alguien compile el sistema y nuestro sistema de integración contínua (Hudson), se encargará de hacerlo todas las noches.

Y habría tardado mucho menos si hubiesemos hecho TDD desde el principio, ya que o bien no se habría producido el error, o bien ya habría tenido montada la base para el test automático y sólo habría tenido que añadirle más cosas.

Mar 21

Jugando con TDD, MVP, PF, EasyMock y Mockito

 

Normalmente no codifico, lo he dejado por imposible. Un día de trabajo mio normal consiste en atender una cola de gente que viene a preguntarme cosas, algunos pocos sobre java, algunos más sobre las herramientas que usamos (maven, archiva, redmine, etc) y muchos (jefes sobre todo) sobre incidencias de los proyectos, cómo va el trabajo, etc. En esas condiciones, es muy difícil concentrarse cinco minutos seguidos y mucho menos diez, por lo que hace tiempo que decidí no meterme en serio a codificar.

Sin embargo, el viernes de la semana pasada fue un día excepcionalmente tranquilo. Todo el mundo sentado en su sitio, a sus cosas y sin que nadie viniera a decirme nada. Cuando pasan estas cosas, suelo aburrirme un poco, no empiezo a hacer nada en serio por temor a las interrupciones que seguro que están al caer. Pero ese viernes la tranquilidad se estaba prolongando demasiado, había un módulo nuevo para hacer desde cero y yo acababa de leerme algo sobre TDD, así que hablé con la persona que tenía asignada ese módulo y no le molestó que lo empezara yo (aun a sabiendas de que lo dejaré a medias y le tocará seguir a él). Así que así me he tirado casi una semana, codificando y jugando al TDD.

Las primeras historias fueron más o menos sencillas. Hice mis test, luego mi código, refactoricé un poco y vuelta a empezar. Pero llegó un momento en que el tema se me complicó más de la cuenta. Llegó una historia de usuario en la que había involucrada una parte de interface de usuario con Swing que no era trivial de probar con un test automático. Y me puse a investigar en internet.

Primero encontré la posibilidad de ponerse con los métodos de java getComponents() y similares para tratar de buscar el componente java (el JTextField, el JButton o lo que sea) que necesitas para tu test y a partir de ahí usar el método setText(), getText() o doClick() para simular la interacción con el usurio. Aunque eso sí te puede sacar del apuro en un caso puntual, no parece que sea la mejor forma de hacerlo por sistema. El test se llena de código rebuscado con bucles y recursiones hasta localizar el componente que te interesa y que por supuesto, tienes que haber dado previamente un nombre con el método setName() para distinguirlo de otros componentes del mismo tipo.

Seguí buscando y encontré que todo ese proceso se facilita con librerías como FEST-Swing, que de alguna forma facilitan y eliminan todo el código raro que mencioné en el apartado anterior. No llegué a probarlo porque no encontré un repositorio maven que me permitiera bajarme la librería fácilmente.

Y seguí buscando y encontré una cosa interesante: El patrón MVP (Model-View-Presenter) y el PF (Presenter First). La idea de estos patrones es que una interface gráfica de usuario es muy compleja de testear automáticamente, por lo que debemos hacerla lo más tonta posible, para no tener que testearla. Es decir, un panel de nuestra interface gráfica de usuario únicamente debe tener métodos set y get para mostrar datos y recogerlos. No debe tener actionListeners para los botones ni ningún tipo de inteligencia. Toda la inteligencia de esa interface se hace en otra clase, llamada Presenter. Esta clase debe recibir dos interfaces en el constructor, una de la interface gráfica de usuario y otra correspondiente al modelo de datos. La clase Presenter debe hacer ella misma de listener de los botones y encargarse de interactuar con el modelo y la interface de usuario. Haciéndolo así, es fácil testear toda esta lógica, ya que podemos instanciar en nuestro test el Presenter y pasarle dos mock-objects, el del modelo y el de la interface del usuario.

Me gustó la idea, así que rápidamente me puse a modificar mi código para usar un Presenter y quitarle el máximo posible de código (todo) a los componentes swing. Por supuesto, siguiendo TDD, primero comencé a hacer test y modificar los ya hechos.

Pero lleguó el momento de usar los mock-objects. Por un lado, podía hacérmelos a mano, pero por otro lado hace tiempo que conozco y nunca he usado la librería EasyMock. Supuestamente (y en la realidad) facilita la creación de los mock-objects, haciéndolo automáticamente. Viendo la documentación me pareció fácil de usar y encontré además la librería en los repositorios maven, así que me puse con ello. Sin embargo, me ha decepcionado un poco. Los EasyMock están bien para determinadas cosas, pero les veo dos pegas que no los hacen ideales en todas las circunstancias:

  • La filosofía de un EasyMock en el test es crear el mock-object, decirle a qué métodos se van a llamar durante el test y con qué parámetros, luego llamar a su método replay() para indicarle que empieza el test de verdad y hacer el test. Si durante el test no se sigue la secuencia de llamadas indicada anteriormente, el test falla. ¿Cual es la pega que le veo a esto?. Pues sobre todo para cosas como TDD, en que las clases se refactorizan y van evolucionando según se van desarrollando nuevas historias. Es muy fácil que sin modificar el funcionamiento de la clase, la secuencia de llamadas cambie, porque se hagan más llamadas, o se reemplacen algunas por otras o lo que sea. Esto obliga a ir rehaciendo los test, aunque la funcionalidad no cambie, simplemente porque cambia el orden o el número de llamadas que se hacen.
  • Los EasyMock, aunque no lo puedo asegurar porque no lo he mirado en profundidad, no permiten que un mock-object genere algún tipo de eventos. En el caso concreto del patrón MVP y PF mencionados antes, un Presenter añade un ActionListener a un botón de la interface de usuario. Si estoy testeando el Presenter y le paso un mock de la interface de usuario, no puedo simular un click en el botón o hacer la llamada al actionListener. Unicamente puedo ver que el Presenter efectivamente se suscribe llamando al addActionListener() correspondiente.

Tratando de solucionar algunas de estas pegas (la primera sobre todo), hay otra librería, Mockito, que se basa en EasyMock, pero trata de darle una API más sencilla y versátil. Tengo pendiente echarle un ojo, pero creo que la segunda pega no me la va a solucionar. Supongo que para cosas como los listener, no me quedará más remedio que hacerme los mock-object a medida … y a mano.

Lo que me da realmente pena de todo esto, es la persona que va a seguir con este código, cuando a mí empiecen a agobiarme con otros temas. Veamos que cara pone cuando empiece a ver Presenters y Mockitos. O lo tira todo a la basura, o aprovecha para aprender una nueva forma de hacer las cosas y así, al menos, tendré alguna opinión más sobre el tema, aparte de la mía propia. Esto es, nuevamente, una sopa de piedras o un capitán araña. Ya veremos en qué acaba.

Oct 18

¿TDD requiere memoria de elefante?

 

A mí siempre me ha gustado programar de poco en poco, haciendo un trozo de código que se puede arrancar y ver, luego añadir otro trozo que también se puede arrancar y ver y así sucesivamente. Nunca me ha gustado liarme a escribir código durante varios días, sin probar nada, y luego arrancarlo y pasarme otros varios días haciéndolo funcionar. No sé qué es mejor, pero desde luego, me gusta más lo primero, es más entretenido. Tiene la pega de que "pierdes", sobre todo en proyectos grandes, mucho tiempo compilando. Haces algo en uno o dos días, compilas y pruebas, repites, y repites.

Por todo esto, TDD es una metodología que me gustó desde que la descubrí. Básicamente dice que hay que hacer lo que yo hago, implementar un poco y probar. Sin embargo, TDD es por supuesto más estricto y organizado. Antes de programar, debes hacer unos test para ver que fallan. Luego debes implementar tu código para que no falle y finalmente debes refactorizar para que tu código si adapte bien al resto del código y viceversa.

Así que he tratado de adaptar mi forma de trabajo a TDD. Pero me encuentro con un problema con la refactorización. No es lo mismo ir haciendo código y refactorizar según vas viendo código chapucero, que esperar a tener algo funcionando y luego refactorizar para adaptar tu código o el código ya hecho el uno al otro. Mi principial problema es la memoria de mosquito. Una vez tengo mi código, ya no me acuerdo con detalle de cómo estaba hecho el anterior, y no sé qué tengo que refactorizar para que se adapten el uno al otro. No me queda más remedio que dar un repaso a lo ya hecho para ver qué juntar.

Supongo que a la larga incluso es mejor, ya que el repaso del código para mejorarlo tiene que ser bueno, pero no deja de ser un poquito pesado.