Sep 20

He leído “Test Driven: TDD and Acceptance TDD for Java Developers”

tdd acceptance testBueno, realmente exagero un poco cuando digo "he leído…", me he dejado sin leer los últimos capítulos.

La parte central del libro me ha encantado, pero la primera parte y la última me han resultado muy pesadas e inútiles, hasta el punto de dejar de leerlo en esa última parte.

Los primeros capítulos nos cuenta principalmente las ventajas de TDD, no se extiende mucho en qué es o cómo se hace TDD, sino que se extiende mucho (muchísimos) en sus ventajas. Estas ventajas son más o menos conocidas por todos (código con menos fallos, confianza en que no estropeamos nada a la hora de hacer refactoring por lo que nos cuesta menos hacerlo, etc, etc). Por ello, varios capítulos dedicados a las ventajas me parece excesivo e incluso repetitivo, ya que una y otra vez comenta las mismas ventajas.

Afortunadamente el grueso de capítulos centrales me ha parecido una maravilla. No por TDD en sí mismo, sino porque se dedica para cada tipo de proyecto diffícilmente testeable (base de datos, jsp, swing, etc) a mostrarnos los distintos frameworks con los que podemos trabajar (jdbc, hibernate, spring, jsf, tapestry, wicket, …) como las librerías útiles para hacer test automáticos en esos frameworks (mockito, clases de spring que son mock objects de interfaces java complejas, jspunit, fit, etc, etc). Lo mejor de todo esto es que no da por supuesto que conocemos cada framework o librería, sino que nos da un resumen de cada uno de ellos, qué es, para qué sirve y cómo se usa. Así que esta parte, más que para hacer test automáticos, sirve realmente para conocer de qué van todas esas siglas que oímos de continuo y a veces no sabemos qué son (jsf, spring mvc, tapestry, jsf, wicket …) y es una primera guía para saber por dónde empezar a usarlas.

Dentro de este grupo de capítulos nos habla de los test de aceptación, que son test automáticos de más alto nivel donde idealmente se considera el sistema como caja negra y se testea desde fuera automáticamente. Idealmente estos test deben estar escritos por el cliente más que por los desarrolladores, puesto que el cliente es el que sabe lo que quiere y si el test es o no suficiente. Así que, aparte de discutir en qué casos se puede/debe testear desde fuera el sistema como caja negra, o cuando se puede/debe testear justo por debajo de la interfaz de usuario, nos introduce en herramientas como fit o fitnesse.

En la última parte, la que he dejado de leer, nos muestra los problemas que podemos tener con nuestros compañeros de trabajo si intentamos convencerlos de que usen TDD, y cómo identificar esos problemas y cómo abordarlos. Pero para mí, programador principalmente, lo de las relaciones humanas no es un libro que me entretenga. Y para mí, cabeza cuadriculada, semejante texto me parece demasiado "etéreo" y evidente. Oír por enésima vez las ya consabidas frases estilo "para que tus compañeros hagan TDD, dales ejemplo haciéndolo tú" o "si te dicen que sí sin entusiasmo igual te están diciendo que no", no me parece que ayuden demasiado a pelearte con los problemas día a día. Este tipo de problemas son problemas que puedes resolver si tu forma de ser es la adecuada para ello, y los resolverás o no independientemente de que hayas leído este libro. Hay quien de forma innata es un lider y que yo sepa, no existe quien de forma innata es un anti-lider y se convierte en lider con un cursillo.

Hablando de cursillos, me ha llamado la atención (creo que tiene toda la razón), este post sobre el peligro de las certificaciones, tan de moda hoy en día http://www.javiergarzas.com/2012/09/problemas-testing.html

Y volviendo al libro otra vez, una frase traducida más o menos libremente que me ha llamado la atención "Los buenos programadores sufren un tipo especial del síndrome de déficit de atención, consistente en poner todo su empeño en usar una herramienta nueva, para abandonarla pocos meses después  y poner nuevamente todo su empeño en otra herramienta más nueva". Real como la vida misma.

Aug 13

Configurando Hibernate para test unitarios.

Cuando hacemos test de JUnit o la herramienta que sea, una de las cosas difíciles de probar son las transacciones con base de datos. En nuestro entorno de desarrollo necesitaríamos una base de datos igual que la de producción y deberíamos borrar todos los datos, meter los datos previos al test para tener la base de datos en un estado conocido, hacer el test y luego revisar el contenido de la base de datos. Esto, aparte de complejo, puede ser muy lento con una conexión a una base de datos real.

Una de las soluciones es utilizar una base de datos en memoria (como HSQLDB, H2, etc). Estas bases de datos suelen ser un jar que no necesita instalación, así que basta con añadirlo a nuestro classpath en los test. Al ser en memoria, los test serán también muy rápidos. Sin embargo, sigue habiendo dos problemas:

  • En cada test habría que crear todas las tablas desde cero.
  • Las SQL de estas bases de datos pueden no ser las mismas que la de nuestra base de datos de producción (MySQL, Oracle, etc).

Para ayudarnos a solucionar estos problemas podemos usar Hibernate (u otra herramienta similar). Si en el fichero de configuración de Hibernate, los de mapeo de clases (o anotaciones) y nuestro código no usamos nada específico de una base de datos (debería ser lo normal salvo casos excepcionales), Hibernate nos hará independientes de la base de datos que usemos (de hecho, ese es uno de sus objetivos). Por otro lado, hibernate tiene un parámetro de configuración hibernate.hbm2ddl.auto en el que le podemos indicar que cree desde cero todas las tablas de la base de datos. Así que Hibernate nos resuelve, en principio, los dos problemas mencionados.

La primera idea que se nos ocurre es tener dos ficheros de configuración de Hibernate (hibernate.cfg.xml), uno para producción con la base de datos real y otro para test. Pero somos buenos programadores y no nos gusta tener cosas repetidas (principio DRY), posiblemente esos ficheros sean prácticamente iguales salvo los parámetros de conexión a la base de datos y el mencionado parámetro hibernate.hbm2ddl.auto, que en nuestro base de datos de test valdrá "create" y en la de producción puede valer algo como "verify".

La solución es simple, basta tener un único fichero de configuración para la base de datos de producción, con toda la parámetrica real. Para los test cargamos ese fichero … y modificamos en código sólo lo que nos interese. Se puede hacer de esta forma

Configuration configuration = new Configuration().configure();
configuration.setProperty("hibernate.connection.url", "jdbc:h2:mem:databaseName");

configuration.setProperty("hibernate.hbm2ddl.auto","create");
SessionFactory sessionFactory = configuration.buildSessionFactory();
 

El método configure() de Configuration leerá el fichero por defecto de configuración (suponemos que es el de producción) y luego, con el método setProperty() podemos "machacar" todas las propiedades que queramos de ese fichero. En el ejemplo sólo hemos cambiado la url de la base de datos y hibernate.hbm2dll.auto, aunque posiblemente deberíamos cambiar también el nombre de la clase del Driver de base de datos, usuario, password, etc.

Ahora sólo nos quedaría poner este código en algún sitio al que nuestras clases de test puedan acceder. Nuestras clases de test obtendrían el sessionFactory usando este código, mientras que el código de producción lo obtendría de la forma habitual, posiblemente a través de un UtilHibernate.java.

Por supuesto, no nos libra nadie de llenar la base de datos en cada test con unos datos conocidos y analizar luego los resultados.

Nos queda un detalle, que es borrar los datos después de cada test. Utilizando la base de datos en memoria como hemos hecho en el código anterior, quedará "viva" y con datos mientras esté arrancada la máquina virtual java. Al ejecutar una batería de test (con mvn test, por ejemplo), la máquina virtual no muere hasta que acaban todos los test, por lo que los datos que unos test van dejando en base de datos quedan para los siguientes test, aunque estén en memoria. Debemos vaciar esos datos después de cada test.

Una opción "pesada" es ir borrando los datos o hacer un "drop" de las tablas, en el orden adecuado para evitar problemas de "contraints". Pero otro método rápido y sencillo consiste en simplemente cerrar el SessionFactory. El siguiente test tendrá que crear un SessionFactory nuevo (con el código de arriba) y así se volverán a crear todas las tablas desde cero. Es decir, nuestras clases de test tendrán los métodos setUp() y tearDown() (o @Before y @After en JUnit 4) de la siguiente forma

public class TestUno extends TestCase {
   private SessionFactory = sessionFactory;

   @Override
   public void setUp() {
      sessionFactory = // Obtener session factory con el codigo anterior
   }

   @Override
   public void tearDown() {
      sessionFactory.close();
   }
   …
}

 

Simplemente un detalle. De esta manera realmente no se borran los datos al terminar en test. En realidad se borran al empezar el siguiente test, ya que el "create" de hibernate.hbm2dll.auto hace exactamente eso, borrar para crear desde cero.

 

Jul 07

Triangulación con TDD

Test Driven Practical TDD and Acceptance TDD for Java DevelopersEstoy leyendo ahora en mis ratos perdidos de tren al trabajo el libro Test Driven: TDD and Acceptance TDD for Java Developers. Los dos primeros capítulos me han resultado muy pesados porque no paran de repetir una y otra vez las ventajas de TDD de mil formas distintas, ventajas por otra parte ya bastante conocidas. Pero acabo de entrar en la parte donde se va desarrollando código con TDD y me está pareciendo interesante.

Una de las cosas que me ha llamado la atención es lo de la "triangulación" con TDD. Ya había leído de ella sin llegar a entenderla realmente, simplemente porque los ejemplos que había leído de triangulación eran muy tontos. Por ejemplo, si tienes que hacer un test de una clase que suma, el primer test podría ser el típico

assertEquals(4, sumador.suma(2,2));

Explicando la triangulación te dicen que pongas directamente que ese método suma(2,2) devuelve a piñón fijo 4 y luego, triangulando, haces otro ejempo de suma, por ejemplo, suma (2,5) y así llegas a la implementación correcta return sum1+sum2. Como se puede ver, algo un poco "estúpido".

Este libro menciona la triangulación varias veces, pero en un párrafo deja muy claro qué es exactamente o, al menos, así me lo ha parecido a mí. El ejemplo que comenta es que queremos tratar varias tipos de tarjetas de crédito (visa, master card, dinners club, etc, etc). El problema es que cada tarjeta tiene sus particularidades y que podemos no tener muy claro cómo hacer el código para tratar todas esas particularidades, qué partes van a ser comunes y ponernos a pensar a priori todo ese código puede ser tedioso.

Así que la solución es la famosa triangulación. Cogemos una de las tarjetas y empezamos el TDD con ella, los test, el código, específico para esa tarjeta, sin preocuparnos de las otras y finalmente el refactoring. Luego cogemos la segunda, hacemos nuevos test para esa y modificamos el código para que funcione para las dos. En la parte importante de refactoring es donde realmente arreglamos ese código para que las partes comunes y no comunes queden bien diseñadas para esas dos tarjetas. Luego tercera tarjeta, más test, más tocar código y lo más importante, nuevamente refactoring para que el diseño sea lo más claro y mejor posible para tres tarjetas.

De esta forma, deberíamos llegar a una de los mejores diseños posibles para tratar los tipos de tarjeta que debemos tratar.

Así que cuando hacemos TDD, más que triangular siempre a piñón fijo, incluso para suma(2,2) haciendo que devuelva 4 a piñón fijo y obligarnos a hacer otro test suma(2,5), debemos aplicar triangulación cuando tenemos varios casos similares pero con peculiaridades cada uno de ellos y no tenemos muy claro cómo hacer un código elegante o el mejor diseño para tratar esas particularidades a priori.

 

Jul 02

¿Qué habría pasado de usar TDD?

hommer pensando en un programa softwareSiguiendo un poco con el post anterior, hace tiempo en Verlo para creerlo comenté un código real de una empresa con el que me había tropezado. En ese código había una clase (llamémosla Datos) con 16 atributos estáticos iguales (digamos, atributo1, atributo2, … atributo16). Pulsando un botón (uno por atributo) debía mostrarse en una ventana nueva algunas cosas relativa a uno de esos atributos. En el código había 16 clases Ventana, una por atributo, llamadas Ventana1, Ventana2…. Ventana16. La única diferencia en el código de esas clases, aparte del nombre, era que accedía a Datos.atributo1, Datos.atributo2 … Datos.atributo16

¿Qué habría pasado si hubieran usado TDD?

Supongamos que este mismo programador hubiera hecho este código usando TDD. ¿Qué habría pasado?. Pues lo evidente, habría 16 clases de test llamadas TestVentana1, TestVentana2, … TestVentana16.

Pero TDD no es sólo hacer test, es hacerlos antes. Bueno, supongamos que los ha hecho antes.

Y TDD tiene otro tercer paso, refactorizar para quitar todas las repeticiones posibles de código, incluso las menos evidentes. Bueno, no sé vosotros, pero yo, independientemente de usar o no TDD, me repatearía hacer copy-paste 16 veces de una misma clase y como ser vago a veces es una virtud, habría dado las vueltas necesarias para no hacer esto. Sin TDD se me ocurre simplemente meter los atributos en un array de 16 posiciones y en un método set() de la única clase VentanaUnica pasarle el índice de la posición que debe tratar. Es lo primero que se me ocurre, nada complejo, seguramente hay más y mejores soluciones.

Sin embargo, este desarrollador no lo ha hecho. ¿Por qué?. Se me ocurren cuatro posibles motivos:

  1. Totalmente inexperto en java, un array es algo complejo de usar y lo del set() ni lo cree posible. Muchos programadores novatos tiene problemas para hacer que los atributos de una clase se vean en otra y por eso tienden a hacerlos estáticos (justo como ha hecho este señor).
  2. No tiene cabeza para programar, por más vuelta que le ha dado, no se le ha ocurrido cómo evitar hacer las 16 clases.
  3. Le importa tres pepinos. Para qué se va a complicar la vida si con 16 clicks de ratón (un copy y 15 pastes) lo arregla.
  4. Todas las anteriores.

Bueno, pues con este panorama, ¿qué habría hecho al intentar refactorizar con TDD?

  1. Ya tengo mi TestVentana1, así que hago mi Ventana1. Ahora mi TestVentana2 y hago mi Ventana2. ¡¡ Código repetido !!. ¡¡ Vamos a refactorizar !!: Imposible, java no permite hacerlo, si java no permite pasar el atributo de la clase Datos a la clase Ventana, Ventana1 y Ventana2 deben ser clases distintas. Y no te digo juntar los dos test en uno solo.
  2. Buff, qué dolor de cabeza, no se me ocurre como puedo convertir dos clases distintas que manejan atributos distintos en una sola.
  3. Jó, que rollo refactorizar ahora que me está funcionando, voy con los siguientes 14 pastes, que el copy ya lo tengo hecho.
  4. Java no deja, no se me ocurre como hacerlo y voy a correr un montón si reaprovecho el copy para el resto de pastes.

Algo como Srcum tampoco evitaría estas cosas. En el sprint diario este señor diría "Ya tengo las ventanas de  los atributos" y todos felices. Bueno, con un poco de suerte, un día diría "tengo la Ventana1 del atributo1", al día siguiente "tengo la Ventana2 del atributo2" y alrededor del quinto día, quizás a alguien se le ponga la mosca detrás de la oreja y quiera ver qué está haciendo. De todas formas, no se tardan 16 días en hacer 15 pastes.

Una herramienta de análisis estático de código integrada en una herramienta de integración continua cantaría esto por la noche, suponiendo que cante cuando encuentra código repetido y, como hacemos nosotros, el compilado falla si no se cumple alguna métrica importante. Desgraciadamente, existe el @SuppressWarnings que la gente se acostumbra a poner por sistema, incluso antes de que cante la métrica (conozco al menos dos personas que lo hacen).

La programación en parejas también habría ayudado, salvo que la pareja de nuestro programador fuera el recién entrado al que le han asignado para que le enseñe.

Jul 01

Metodologías ágiles y tradicionales. ¿De verdad sirven para algo?

el big-mac de las metodologiasBueno, parece que últimamente las malas son las metodologías tradicionales y las buenas son las metodologías ágiles, pero a fin de cuentas, ambas son metodologías y las metodologías, en general, presentan sus problemas. ¿Por qué?. Este artículo de Joel on Software nos da la respuesta.

Un cocinero excepcional hace lo siguiente:

Ahora bien, el Chef Desnudo no sigue ningún apestoso Manual de Operaciones. No mide nada. Mientras esta cocinando, usted ve un frenesí de comida sacudida y siendo arrojada por aquí y allá. "Simplemente pondremos una pizca extra de romero por acá, que no lo arruinara, y le daremos una buena sacudida", dice el. "Amásenlo así. Perfecto. Solo espárzanlo por todos lados." (Si, de verdad se ve como si simplemente lo esparciera por todos lados.

es decir, una persona brillante (cocinero en este caso), no necesita seguir ninguna metodología ni medir nada para obtener un producto genial. ¿Por qué las metodologías?. Pues porque gente brillante hay poca y del resto hay un montón. Para conseguir que el resto de la gente haga un buen producto, la persona brillante escribe una serie de reglas para que la gente del montón sea capaz de hacer lo mismo que él … y eso no funciona. Si se hace, se tienen "Big Macs". El resumen del proceso es el que indica Joel on Software en el mismo artículo

1. Algunas cosas requieren de talento para hacerse realmente bien.
2. Es difícil tener talento a la medida.
3. Una forma en que la gente intenta igualar el talento es haciendo que el talentoso cree las reglas para que los menos talentosos las sigan.
4. La calidad del producto resultante es muy baja. 

¿Y qué pasa con el software?. Pues más o menos lo mismo. Unas personas geniales escribieron unas metodologías para hacer buen software, sean las tradicionales, sea Scrum, sea TDD y el resto de los mortales tratamos de seguirlas. ¿Y qué pasa?. Pues cosas como esta

Total, que para hacer bien Scrum o hacer bien TDD o cualquier otra de las metodologías ágiles hay que hacer un cambio importante de mentalidad, ser bueno en muchas materias incluido como programador, tener mucho sentido común, etc, etc.

Y digo yo… la persona que cumple todo eso y puede hacer realmente bien TDD/Scrum/metodologías ágiles… ¿necesita realmente hacer todo eso?. Estoy totalmente convencido que un programador genial es totalmente capaz de hacer un muy buen software sin preocuparse en absoluto por ninguna metodología concreta, sino siguiendo simplemente su intuición. Y estoy convencido que el resto de los mortales estamos predestinados a hacer mal software directamente o hacer mal software después de haber seguido deficientemente una metodología ágil.

A pesar de todo, es mejor seguir una metodología que ninguna. Imagina en el McDonalds si cada uno se hace los BigMac de cualquier manera. Pero aunque lo he dicho muchas veces, me reitero. Si se quiere un buen software que destaque, el punto más importante a tener en cuenta es elegir a los mejores programadores.

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.