Cuando escribes código (o cuando lo estás probando) te encuentras realidades que tienen distintos efectos en tu trabajo: desde hacerte perder (un poco de) tiempo a convertir tu sistema en un auténtico infierno donde no quieres pasar ni un minuto más del estrictamente necesario.
Una variedad especial que se me ha repetido continuamente son las relacionadas con el tiempo. Por diversos motivos, tratar con el tiempo en un sistema puede llegar a ser extremadamente complicado. La cosa se complica todavía más cuando olvidamos tener en cuenta los detalles más básicos, lo que nos lleva a la motivación de este artículo.


Este post es un resumen, sin seguir ningún orden particular, de algunos de los descuidos, malentendidos y paradojas que me he encontrado al trabajar con sistemas que trataban con fechas, horas y el paso del tiempo.
Lo que sigue es bien conocido por todo el mundo y cualquiera podrá decir, con razón, que olvidar estas cosas es de tontos. Sin embargo, me he visto más de una vez empantadado en mi propio software por culpa de ellas.

No todos los meses tienen el mismo número de días

Es algo que todos saben y, sin embargo, no es infrecuente encontrarte código que, por ejemplo, cuando queremos saber qué día es exactamente dentro de un mes hacen cosas como sumar siempre 30 días a la fecha actual (o sumar siempre 31 días que, para el caso, es igualmente inválido con carácter general).
Y es que mayo tiene 31 días, junio tiene 30, julio tiene 31 y agosto tiene también 31. Y, por supuesto, tenemos febrero, que tiene 28 días, la mayoría de las veces aunque...

No todos los febreros tienen 28 días

Algo que normalmente se aprende en la escuela de pequeño y que se suele olvidar todavía con más frecuencia que el apartado anterior. Pero sí, resulta que, por una mezcla de cienca e historia, algunos meses de febrero tienen 28 días y otros tienen 29. Y, como, colorario...

No todos los años tienen 365 días

Por el mismo motivo por el que hay meses de febrero más largos que otros (y como consecuencia de ello), hay años que tienen 366 días. Se llaman bisiestos, ocurren cada cuatro años y hasta podemos hacer un sencillo cálculo para saber si un año es bisiesto.
Pero no siempre lo tenemos esto en cuenta y, más veces de las que deberíamos, damos por sentado que, por ejemplo, si tenemos que programar algo para que se ejecute automáticamente dentro de un año exactamente, podemos sumar 365 días a la fecha actual y tendremos la fecha en que queremos que nuestra tarea sea ejecutada (error).

Y no todos los minutos tienen 60 segundos

Y por eso hay que meter un segundo aquí y allá de vez en cuando -aunque nunca hayas tenido que tratar con este problema directamente, hay quien se lo toma muy en serio.
De acuedo, éste ha sido un poco más friki de la cuenta. Volvamos con algunos errores más ordinarios, por ejemplo, cuando tratamos con intervalos de tiempo. Empecemos por uno muy simple.

Un intervalo de tiempo de una hora no siempre empieza y acaba en el mismo día

Como ocurre, por ejemplo, con el intervalo de una hora que empieza a las 23:48 de hoy, que terminará mañana. Sencillo,¿ verdad? No sé por qué me he encontrado código que daba por hecho lo contrario (por ejemplo, asumiendo que al sumar una hora a la hora actual tendrías otra hora en el mismo día que tenías al principio).

Tampoco todos los intervalos de una hora empiezan y terminan el mismo mes

Si, por ejemplo, el intervalo que empieza a las 23:48 resulta que lo hace el 30 de abril. Así que, ya sabéis, sumar una hora a un instante de tiempo puede dar lugar a un instante de tiempo en otro mes distinto. Más aún...

No todos los intervalos de una hora ni siquieran empiezan y terminan el mismo año

Cuando el intervalo del que hablamos antes, en vez de ser el 30 de abril, es el 31 de diciembre, ya la hemos liado.
Bueno, creo que ya he explotado bastante el ejemplo y la idea se capta. Pero, ¡ah, recuerda!
  • No todas las semanas empiezan y terminan el mismo mes
  • Ni el mismo año -eso de que el uno de enero caiga en lunes es lo menos frecuente. De hecho, no volverá a ocurrir hasta el 2018.
Es momento ahora de ponerse un poco más técnico. La siguiente es obvia pero provoca bastantes problemas.

El reloj del sistema no siempre tiene la hora correcta

Y es que hay muchas formas de que te dé una hora equivocada. Por ejemplo cuando

El reloj del sistema está configurado como si estuviera en una zona horaria diferente

Y es que al que montó la máquina no le pareció importante asignar la zona horaria correcta cuando instaló el sistema operativo en el servidor de pruebas. Así que la hora parecerá la misma, y servirá para casi todo igual, pero llegará un momento en que tengas que tener en cuenta la zona horaria y, de repente, todo empezará a parecer que ocurre dos horas antes (o dos horas después, o lo que sea) que cuando debería ocurrir.
O también puede ser que sencillamente...

El reloj del sistema puede ser aleatoriamente inconsistente

No tiene sentido, pero ves que el sistema está configurado con un reloj exactamente adelantado 1 hora y 37 minutos y eso ha hecho que pierdas dos horas intentado encontrar la causa de que las horas de los cambios hechos en una tabla de tu base de datos sean incorrectas. Y entonces es cuando uno que pasa por ahí te dice que ayer cambió la hora del sistema porque quería ver si un proceso batch que normalmente se ejecuta a las seis de la tarde terminaba correctamente y se tenía que ir a casa a las cinco y cuarto.
Hablando de relojes cambiados, recuerda que:

La hora en tu servidor no tiene que coincidir con la hora que tienen los clientes

Y es que quien se conecta puede ser un mentiroso y haber cambiado su reloj para que parezca que solicitaron una oferta antes de que terminara el plazo en que expiraba.
O puede que sencillamente ellos viven en Sindey, tú en Madrid y tu servidor está en alguna parte de Irlanda. Lo que nos lleva a un subtipo especial.

Los relojes de tu servidor y de los clientes no siempre están en la misma zona horaria

Así que usa la differencia entre ambas zonas horarias para hacer tus cálculos. Y cuando los hagas, acuérdate también de tener en cuenta en qué día y qué mes estamos porque...

La diferencia entre regiones situadas en zonas horarias distintas no es siempre la misma

Puede variar a lo largo del año, por ejemplo, si en una zona aplican el horario especial de verano mientras que en otra, no.
Y es que trabajar con el tiempo nunca fue fácil y son muchas las cosas que hay que tener en cuenta, por lo que no es tan infrecuente que pasemos por alto algo que luego nos dará problemas. Espero que ahora que las he puesto por escrito no se me olviden más.


Supongamos que estamos construyendo un sistema para la gestión de clientes donde, entre otras cosas, vamos a registrar las compras que realizan. Empezamos con un modelo muy básico, creando una clase para nuestros clientes que nos ofrecerá la funcionalidad necesaria para cumplir nuestra primera regla de negocio:

  • Son clientes senior aquellos que tienen más de año de antiguedad
Aquí tenemos algo de código:

Hemos empezado con un test que falla y continuamos desarrollando la implementación que hace que el test pase:

Seguimos con nuestro modelo. Como dijimos, los clientes compran productos, para los que tenemos un código y la fecha de compra (dicha fecha se establecerá en el momento en que la compra se haga efectiva, no pudiendo modificarse posteriormente). Así que introducimos una nueva clase para productos que ofrecerá una funcionalidad, bastante básica también, con otra regla de negocio:

  • Un producto ha sido comprado recientemente si la compra se produjo en el último mes
Y creamos un test para dicha funcionalidad:

A continuación, añadimos la lógica que hace que nuestro test pase:

Ahora que tenemos clientes y productos, vamos a añadir la lógica que permitirá a los clientes comprar productos. Empezamos, como siempre, por un test que falla:

Y, después de haber creado el test, implementamos dicha lógica y hacemos que el test pase:

Con el código que tenemos a estas alturas, podemos empezar a hacer cosas más interesantes. Nos proponemos hacer ofertas especiales a algunos clientes y de ahí sale nuestra tercera regla de negocio:

  • Sólo los clientes senior pueden recibir ofertas especiales.
Así que añadimos una nueva funcionalidad en nuestra clase de clientes, empezando con tests que fallan:

Y, a continuación, implementamos esta funcionalidad de acuerdo con nuestra regla de negocio para hacer que los tests pasen:

Nuestro código pasa a produción, la gente empieza a comprar y nosotros hacemos ofertas a nuestros clientes para aumentar las ventas. Al cabo del tiempo, decidimos cambiar las condiciones para hacer ofertas a clientes y modificamos nuestra tercera regla de negocio con una nueva versión:

  • Sólo los clientes senior que hayan realizado alguna compra recientemente pueden recibir ofertas especiales.
Como siempre, empezamos por los tests, así que creamos un nuevo test que nos asegure que esta nueva versión de nuestra regla de negocio se cumple. Pero, con el código de test que hemos creado hasta ahora, nos encontramos con la primera dificultad. La clase Product fue diseñada para ser inmutable, por lo que ni el código del producto comprado ni obviamente la fecha de compra deberían poder ser cambiados. Con nuestro código actual, necesitamos establecer una fecha de compra con más de un mes de antelación para poder construir un test que compruebe que los clientes que

  • tienen más de un año de antigüedad pero
  • no han comprado recientemente
no reciben ofertas especiales.

Aquí empiezan las dificultades pues tenemos que, para poder probar nuestro sistema, hay que violar el diseño que nosotros mismos habíamos definido. No pasa nada, hacemos un pequeño ajuste en la clase Product y, a partir de ahí, resulta mucho más fácil probarlo todo:

Ahora ya podemos implementar nuestra nueva regla en la clase Customer:

Y esto es lo que llaman Design for testability, ¿verdad? Hemos modificado nuestro diseño para poder probar el código correctamente. ¡Falso! Esto no es más que un diseño pobre que hemos enmendado de mala manera para poder crear unas mínimas pruebas unitarias, pero dista mucho de ser una buena solución y el problema no hará más que agravarse con el tiempo. El diseño para la prueba no tiene nada que ver con romper el principio de encapsulación y exponer el estado interno de un objeto permitiendo que actores externos puedan alterarlo sin ningún control.

Por supuesto, podríamos intentar usar algún tipo de objeto mock para los productos que compran los clientes, lo cual requeriría configurar dichos mocks para devolver las fechas de compra que necesitamos en nuestra prueba y modificar la propia clase Customer para poder inyectar dichos mocks como productos comprados por el cliente. Sin embargo, esto sería un engorro que nos va a hacer perder mucho tiempo cada vez que queramos probar algo y ya sabemos que hacer que las pruebas sean complicadas de programar supone abandonar la práctica de programar pruebas automáticas.

El problema no está en si la clase Product es inmutable o no, en cómo modificar la fecha de compra de los productos o en cómo injectar instancias mocks de productos en la clase Customer. El auténtico fallo de diseño en nuestro código está en que hemos dejado en manos de la clase Cutomer la creación de la fecha de compra de los productos. Es decir, cuando un cliente compra un producto, establecemos la fecha de compra del product (que debe ser el instance actual, es decir, ahora) y dejamos que sea la propia clase Customer la que decida cuándo es ahora.

El verdadero problema está en que la clase Customer es  responsable de determinar cuándo es ahora, es decir, de instanciar Date.
Un verdadero diseño que facilite las pruebas consiste en delegar la lógica que determina cuándo es ahora, es decir, crear una nueva instancia de la clase Date en el instante actual, a una clase externa que sirva como proveedor de tiempo. Con esta idea, la implementación no podría ser más sencilla:

Ahora ya no es Customer la clase responsable de crear objetos Date sino que los solicita a una clase externa, TimeProvider. De hecho, a partir de ahora ninguna clase en nuestro sistema debería jamás crear sus propias instancias de Date sino pedirlas siempre a TimeProvider.

Habréis notado algo raro en esta implementación de TimeProvider. ¿Qué es ese campo offset y para qué lo queremos? Offset no es ni más ni menos que nuestra implementación del concepto de design for testability. Veamos cómo funciona.

Supongamos, como caso más sencillo, que offset toma un valor de 0. A continuación, solicitamos a TimeProvider la fecha y hora actual llamando al método now(). Pero supongamos que offset tuviera como valor el incremento de tiempo correspondiente a tres días (en milisegundos) y le volvemos a pedir a TimeProvider la fecha y ahora actual, es decir, volvemos a llamar a now(). ¿Qué recibimos? La fecha y hora de dentro de tres días. ¿Y si todo nuestro sistema estuviera conectado a TimeProvider y cada vez que requiriese la hora actual llamara al método now()? Entonces nuestro sistema estaría viviendo en el futuro, tres días en el futuro, para ser exactos.

Para conseguir esto, vamos a construir una clase que solamente pueda ser utilizada por nuestro código de pruebas. Dicha clase nos permitirá ajustar el valor de offset dentro de TimeProvider de forma que siempre sume (o reste) una constante de tiempo al instante actual. Así es como acabamos de crear la máquina del tiempo:

Básicamente esta clase sirve para establecer el valor de offset en TimeProvider. Además le he añadido algunos métodos adicionales para permitir especificar el instante de tiempo al que queremos viajar como Date, LocalDateTime, etc. pero la idea básica es esa: cambiar el offset de TimeProvider. Ahora podemos reescribir nuestro código de pruebas para la nueva versión de nuestra regla de negocio de una forma más clara y sencilla:

Ya no necesitamos cambiar la fecha de compra de los productos ya que ésta se va a establecer en el instante correcto cuando llamamos al método buy(). Nuestra clase Product vuelve a ser inmutable tal y como queríamos. De hecho, todos los tests que hemos creado hasta ahora pueden ser reescritos usando TimeMachine, lo que nos va a ayudar a hacerlos más legibles:

Sólo hay un detalle final que tenemos que tener en cuenta. Como debemos mantener las ejecuciones de los distintos tests independientes unas de otras, es preciso que offset vuelva a cero cada vez que terminamos con una prueba (y también asegurarnos de que es cero cuando una prueba comienza). Para eso, TimeMachine nos proporciona un método reset() que se encarga de ello.

En resumen, hemos llegado a una solución con un mejor reparto de responsabilidades entre las distintas clases del sistema. Esta solución nos ha ayudado a mantener el diseño inmutable que queríamos para nuestra clase Product y ha conseguido que los tests sean más fáciles de escribir y de leer.

Incluso en el caso de que no estemos demasiado interesados en la inmutabilidad de objetos, seguir principios de Design Driven Domain o sencillamente no nos importe llenar nuestras clases de métodos de acceso getters y setters (bueno, hay gente para todo), el tener un proveedor centralizado de fecha y hora para todo el sistema nos puede resultar muy útil cuando estamos construyendo lógica fuertemente ligada a una cronología de eventos y acontecimientos (por ejemplo, cuando estamos implementando un flujo de trabajo y queremos probarlo de forma integral).

Aquí termina la presentación de mi máquina del tiempo particular, una ligera variación del arcano conocimiento que me fue transmitido por Dmytro Iaroslavskyi, un auténtico fenómeno. Espero que os haya resultado interesante esta solución. Por supuesto, cualquier comentario sobre cómo podéis aplicarla, qué modificaciones haríais, qué puntos débiles le veis o cualquier otra cosa que os venga a la mente será más que bienvenida.

Tenéis el código fuente utilizado como ejemplo para esta entrada en github.


Siempre recomiendo utilizar cuantos más componentes ya disponibles mejor. Nunca construyas tu propio framework para:

  • Persistir datos
  • Mostrar información basándote en plantillas
  • Construir la navegación de un sitio web
  • Comunicar componentes remotos
Y tantos otros ejemplos que me vienen a la cabeza.

Sin embargo, hay un framework que siempre deberías construir en cualquier proyecto en el que vayas a participar. Es algo que deberías tener claro antes incluso de empezar. Cuanto antes lo construyas será mucho mejor y no hacerlo pone seriamente en riesgo tu proyecto.

Se trata de tu propio framework para automatizar tus pruebas. No es algo que puedas tomar de alguna otra parte, ni que puedas copiar y pegar de un proyecto pasado (aunque, evidentemente, haber construido uno en el pasado te ayudará a hacerlo ahora). Y no puedes por una sencilla razón: todavía no ha sido inventado. Y el único que puede inventarlo eres tú porque sólo tendrá sentido para ti.

Pero espera un momento entonces. Si sólo va a tener sentido para mí, ¿sigue siendo un framework? Sí y no, lo veremos más adelante. Ahora mejor empecemos con un ejemplo.

Registrando usuarios en una aplicación web

Imaginemos que hemos construido nuestra nueva aplicación web y, como sabemos que vamos a tener que iterar muchas veces sobre nuestro producto para dejarlo a punto, tenemos claro que necesitamos tantas pruebas automáticas como sea posible. De lo contrario, cada vez que cambiemos algo estaremos corriendo un serio riesgo de que el sistema falle (básicamente nos hemos leído algún libro del tipo The Lean Startup, algo sobre desarrollo ágil o, por lo menos, algún artículo en internet publicado en los últimos 10 años. Así que decidimos empezar creando una prueba para una parte fundamental de nuestra aplicación: el login de usuarios.

Como estamos empezando y todavía no sabemos muy bien cómo será el producto final, decidimos hacer una versión muy sencilla (ver ejemplo).

http://cdpn.io/yqihp

Lo sé, es tan fea que casi hace que duelan los ojos. Pero tampoco Google tenía muy buena pinta cuando empezó y fíjate dónde está ahora. Así que no te preocupes, posiblemente cuando vayas por la iteración número 15 podrás pensar en dejarla bonita. De momento, nos sirve. El código HTML de nuestra página sería:

Automatizando pruebas

Podemos crear una prueba automática que haga login en nuestra aplicación usando, por ejemplo, Selenium:

A partir de aquí, podríamos usar este mismo código en cualquier test que requiera el login del usuario y todo estaría bien... hasta que llegásemos al punto en el que queramos cambiar nuestra pantalla de login (iba a ser en nuestra iteración número 15, ¿no?) y tengamos que modificar cientos o, si hemos sido realmente buenos, miles de tests que se basaban en el código anterior.

Mejorando nuestras pruebas automáticas

El problema anterior básicamente tiene su origen en que hemos hecho nuestro código de prueba dependiente de algo que cambia de forma frecuente (especialmente en las etapas iniciales de un proyecto): la interfaz de usuario. Así que algo tan trivial como cambiar el identificador a los elementos HTML de la página de login puede hacer que cientos de tests empiecen a fallar.

La solución, como otras tantas veces, consiste en añadir un nivel de indirección que se encargue de manejar los detalles acerca de cómo está implementada la interfaz de usuario y, al mismo tiempo, ofrezca una interfaz consistente y homogénea con la que puedan interactuar todos los tests que construyamos. Así es como empezaremos a construir el framework que nos ayudará a automatizar nuestras pruebas.

En nuestro caso, aquellos tests que necesiten hacer un login de usuario se podrían escribir de la siguiente manera:

Una vez empecemos con la construcción de nuestro propio framework podemos ir más allá de la simple abstracción de la UI. Por ejemplo, podemos hacer cosas como:

  • Ofrecer un acceso rápido a usuarios ya creados para los que además conocemos determinados parámetros interesantes desde el punto de vista de las pruebas.
  • Implementar procesos largos que son repetidos por muchos tests.
En mi actual proyecto, por ejemplo, tenemos una serie de usuarios prototípicos que son utilizados repetidamente por toda nuestra suite de pruebas. Por ejemplo, Thomas Müller: como buen alemán medio que es, tiene contratado un seguro de responsabilidad civil (también tiene el nombre y el apellido más repetido en Alemania). Así que siempre que alguien necesita un usuario que tenga registrado un seguro de responsabilidad civil durante la construcción de una prueba sólo tiene que hacer:

TestUsersRegistry.getThomasMueller();

O si queremos crearlo desde cero:

TestUsersFactory.createThomasMueller();

O cuando es necesario registrar un usuario nuevo, sin contratos, para probar una funcionalidad disponible para usuarios recieén registrados (tenemos unas cuantas de ese tipo) sólo hace falta hacer:

TestUsersUtils.registerNewUser("John", "Doe", "john.doe@example.com");

En este caso, este método está sobrecargado con diferentes opciones para darnos la flexibilidad necesaria que nos permita registrar usuarios con todas las opciones que sean relevantes desde el punto de vista de los casos de prueba que estamos automatizando. Lo que nos lleva al siguiente punto importante cuando creas tu propio framework de automatización de pruebas: complejidad.

Características de un framework de automatización de pruebas

Por su propia naturaleza, el códido que escribimos cuando automatizamos pruebas tiene (o sería deseable que tuviera) las siguientes características:

  • Es bastante repetitivo. Muchas acciones se repiten a lo largo de múltiples tests (especialmente cuando implementamos diferentes escenarios para una misma funcionalidad)
  • Es lineal (o debería serlo; si no lo es entonces deberías revisar cómo estás implementando tus pruebas)
  • Es fácil de escribir (porque queremos que escribir tests sea algo sencillo que todo el mundo haga de forma regular)
  • Es fácil de leer. Esto es más importante aún porque:
    • Queremos podemos entender más fácilmente qué esperamos de una funcionalidad con echarle un ojo al código que la prueba
    • Cuando un test falla y queremos arreglarlo lo último que queremos es tener que perder un montón de tiempo averiguando qué es lo que el test estaba intentando hacer
  • Se aproxima lo máximo posible al lenguaje natural y utiliza cuantos más términos propios del dominio de nuestro sistema mejor. Esto es básicamente un corolario del punto anterior, pero quería resaltarlo para dejarlo bien claro.
Por todo esto, queda claro dónde tenemos que poner la complejidad: en nuestro framework de automatización de pruebas. Porque las cosas nunca son sencillas y, si queremos tener un código de prueba que sea super sencillo y fácil de entender, tiene que haber un sitio donde se encuentren todas las complejidades inherentes al código que estamos intentando crear.

Por eso, no es ningún problema que nuetro código de automatización de pruebas sea complicado. Tiene que serlo para que nuestras pruebas sean sencillas. La parte positiva es que será un código que escribirás una vez y lo reutilizarás miles de veces (el beneficio es claro).

Recuerda: no se trata de hacer las cosas más complicadas de lo necesario por el gusto de hacerlas complicadas. Pero no hay ningún problema si tu framework de automatización de pruebas empieza a serlo porque tu objetivo es hacer sencillas las miles de pruebas que quieres automatizar.

Esto nos lleva a las características deseables para nuestro framework de automatización de pruebas:

  • Permite escribir muchos tests con poco esfuerzo.
  • Ofrece interfaces claras que pueden ser utilzadas fácilmente por quienes tengan que escribir las pruebas automáticas.
  • Se acerca lo máximo posible al lengaje natural (lo que permite que las pruebas automáticas puedan ser escritas por más personas distintas a los propios desarrolladores del sistema).
  • Maneja todas las complejidades que puedan aparecer cuando tenemos que escribir el código de una prueba.
  • Es lo suficientemente flexible para facilitar la creación de pruebas para todos los escenarios que necesitemos (no sólo para los más sencillos).
  • Incorpora atajos que hagan la vida más cómoda a quienes escriben los tests. Por ejemplo, si hemos parametrizado el método que crea usuarios para que se puedan crear todos los tipos de usuarios con los que nos interesa probar, es perfectamente válida crear métodos adicionales que permitan crear determinados tipos de usuarios de forma fácil (sin tener que pasar ningún parámetro, por ejemplo) aunque eso pudiera considerarse redundante.
Al principio, crear tu framework de automatización de pruebas puede parecer una tarea complicada pero te aseguro que el beneficio es demasiado grande para ignorarla. Además, es parecido a ir al gimnasio: puede costar mucho al principio pero, cuando te acostumbras, no cuesta tanto y, finalmente, verás que lo haces con naturalidad.

Es también una excelente inversión porque, una vez hecho el esfuerzo inicial y puesto en marcha el framework, verás cómo cada vez inviertes menos tiempo en desarrollarlo y perfeccionarlo y, en cambio, pasas más tiempo usándolo y creando más y más pruebas automáticas lo que te ayuda a mejorar la calidad del sistema que estás construyendo (y, de paso, te hacer sentire mucho mejor como desarrollador y ser humano).

Comentarios finales

Aquellos que están familiarizados con temas como automatización de pruebas, TDD,  etc. puede que se hayan sentido un poco confundidos por haber dejado en el aire algunas cuestiones importantes. Como, ¿para qué tipos de pruebas es importante crear nuestro framework? ¿Sólo para pruebas de aceptación? ¿Sólo cuando estamos probando a través de la UI, como en el ejemplo que hemos visto con Selenium?

Lo cierto es que he sido deliberadamente impreciso en este aspecto. En cualquier caso, debemos crear nuestro framework para cualquier tipo de prueba que automaticemos en nuestro proyecto, no importa que sean unitarias o integradas, de aceptación, de sistema... incluso para pruebas de humo.

Siempre que estemos repitiendo el mismo código una y otra vez cada vez que creamos un test y/o que dicho código dependa de cosas que van a cambiar con frecuencia estamos recibiendo una señal de que necesitamos refactorizar nuestro código de pruebas y empezar a crear nuestro propio framework. Es algo que debemos hacer a múltiples niveles y que tenemos que hacer con naturalidad (casi como respirar).

Referencias

Mi libro favorito sobre este tema, y con el que aprendí a escribir pruebas automáticas de una forma sostenible en el tiempo (y no el lío de código espagueti en el que solía convertir mis tests), es xUnit Test Patterns de G. Meszaros. Casi diría que es una lectura obligatoria (de la que ya hablé anteriormente aquí). Si no lo has leído, puedes empezar echándole un ojo al sitio web del libro, donde hay información muy útil.


El tema que vamos a tratar está relacionado con un tipo especial de testing automático que no suele ser frecuente en nuestro trabajo diario: las pruebas unitarias de clases abstractas.
Normalmente somos consumidores de librerías, utilidades y frameworks que nos dan el trabajo ya hecho para que podamos usar sus clases de forma sencilla y sin tener que preocuparnos de nada (o, mejor dicho, de casi nada).
En cualquier framework que analicemos, veremos que existe multitud de interfaces y clases abstractas. Nosotros nos limitamos en muchos casos a ir incorporando implementaciones y subclases concretas para que nuestras aplicaciones funcionen. En estos casos, las pruebas unitarias que automatizamos prueban estas implementaciones concretas y se enfrentan a problemas típicos como eliminar las dependencias de componentes externos (ya sea mediante mocks, stubs o algún otro tipo de test double).
Pero, ¿qué ocurre cuando somos nosotros los que estamos desarrollando un framework? ¿Cómo podemos probar de forma efectiva nuestro código si éste está plagado de componentes abstractos (ya sean interfaces o clases) y no podemos instanciar las clases más básicas que queremos probar?
Algunos pensarán que difícilmente van a tener que desarrollar un framework alguna vez. Sin embargo, esta opción no están tan alejada de nuestras posibilidades. A continuación os presento una forma sencilla de framework que podemos crear nosotros mismos: el patrón Template Method.

Template Method

Wikipedia:

es un patrón de diseño enmarcado dentro de los llamados patrones de comportamiento, que se caracteriza por la definición, dentro de una operación de una superclase, de los pasos de un algoritmo, de forma que todos o parte de estos pasos son redefinidos en las subclases herederas de la citada superclase[1].
La estructura básica de clases que le da soporte sería:


Esto nos recordar a otro patrón de diseño parecido que también habla de algoritmos: el patrón Strategy. La diferencia fundamental entre Template Method y Strategy es que, en el primero, las subclases modifican una parte del algoritmo mientras que, en el segundo, las implementaciones de la estrategia cambian el algoritmo completo.

Como podemos ver, el patrón Template Method nos da la base para construir un framework con la estructura básica de los pasos que hay que seguir para realizar una determinada acción, dejando que sean las clases concretas las que completen los detalles necesarios. Veamos ahora cómo lo vamos a aplicar en un ejemplo y de qué forma podemos hacer pruebas unitarias a la clase abstracta que define el método plantilla.

Pongamos un poco de orden

Vamos a hacer un ejemplo muy sencillo donde aplicaremos el patrón Template Method. En concreto, se trata de una clase que define el algoritmo para ordenar una lista de cadenas de caracteres.


La clase SortAlgorithm define un algoritmo genérico para la ordenación de una lista de cadenas de caracteres básandose en la implementación concreta del método compare, que se encarga de hacer la comparación básica entre dos cadenas de caracteres cualesquiera. Su implementación en Java sería algo parecido a:

    public List<String> sort(List<String> unsortedList) {
        List<String> sortedList = new ArrayList<String>();
        Iterator<String> it = unsortedList.iterator();
        while(it.hasNext()){
            String element = it.next();
            boolean inserted = false;
            for(int i=0;!inserted && i<sortedList.size();i++){
                if(compare(sortedList.get(i), element)>0){
                    sortedList.add(i, element);
                    inserted = true;
                }
            }
            if(!inserted){
                sortedList.add(element);
            }
        }
        return sortedList;
    }

    protected abstract int compare(String a, String b);

Por supuesto, no vamos a dejar nuestro método de ordenación de listas sin probar (eso no lo hacemos nunca, ¿verdad?). Así que, ¡vamos a crear una buena prueba unitaria!

La opción de testing más sencilla que podría llegar a funcionar

Como no podemos instanciar la clase SortAlgorithm por ser abstracta, lo primero que podríamos hacer para poder probarla unitariamente es proporcionar una subclase de prueba que nos dé una implementación por defecto para el método compare. De esta forma, podríamos hacer:

public class SortAlgormithmConcreteTestSubclass extends SortAlgorithm {

    @Override
    protected int compare(String a, String b) {
        return -1;
    }

}

Con esta implementación del método compare, la cadena a siempre se considerará menor que la cadena b por lo que, con nuestro algoritmo de ordenación genérico, los elementos se irán incorporando a la lista ya ordenada siempre al final. Así tendríamos que el algoritmo de ordenación nos devuelve siempre una lista igual que la primera. El test unitario para el método sort sería el siguiente:

public void testAlgorithm() {
    List<String> aList = new ArrayList<String>();
    aList.add("a");
    aList.add("b");
    aList.add("c");
    aList.add("d");
    aList.add("e");
    SortAlgorithm sortAlg = new SortAlgormithmConcreteTestSubclass();
    List<String> anotherList = sortAlg.sort(aList);
    assertEquals(aList, anotherList);
}

Se trata de una buena implementación de una subclase específica de prueba ya que:

  • La implementación de compare es trivial (una línea de código) y nos devuelve un resultado predecible (útil para la prueba que vamos a programar).
  • Hace que probar el método sort sea sencillo pues sólo tenemos que comprobar que lo que nos devuelve es una lista igual que la original.
  • No sirve realmente para ordenar listas de cadenas de caracteres. Es decir, no estamos dando una implementación concreta del método plantilla que sirva para nada. Su propósito es única y exclusivamente permitirnos hacer pruebas unitarias de nuestro algoritmo con lo que no estaremos nunca tentados de utilizar esta implementación en nuestro código de producción.
Sin embargo también tiene algunos inconvenientes:

  • El test es más difícil de entender ya que, quien lo lee, tiene que buscar en la implementación específica de prueba, SortAlgormithmConcreteTestSubclass, para saber qué hace.
  • Hemos tenido que crear una nueva clase únicamente para poder hacer un test. En nuestro caso, ha sido una clase sencilla pero en otros podría ser mucho más complicada. Como, al final, todo el código que tenemos no es más que inventario, debemos intentar reducirlo al mínimo posible. Esto es más verdad aún para el código de test.

Haciéndolo fácil con Mockito

Este proceso lo podemos simplificar mucho utilizando alguna librería que nos ayude en la tarea de crear un comportamiento predeterminado para el método abstracto compare, de forma que así podamos probar convenientemente nuestro algoritmo de ordenación genérico. Y aquí es donde entra en juego Mockito.

En este caso, vamos a utilizar Mockito para dar comportamiento únicamente al método abstracto compare, mientras que seguimos utilizando toda la lógica ya implementada en el método de ordenación, sort. Nuestro test unitario quedaría ahora así:

    public void testAlgorithm() {
        List<String> aList = new ArrayList<String>();
        aList.add("a");
        aList.add("b");
        aList.add("c");
        aList.add("d");
        aList.add("e");
        SortAlgorithm sortAlg = mock(SortAlgorithm.class, CALLS_REAL_METHODS);
        doReturn(-1).when(sortAlg).compare(anyString(), anyString());
        List<String> anotherList = sortAlg.sort(aList);
        System.out.println(anotherList);
        assertEquals(aList, anotherList);
    }

El detalle fundamental en el que tenemos que prestar atención es el haberle pasado al método mock un parámetro extra: CALLS_REAL_METHODS. Gracias a este parámetro estamos construyendo una implementación de la clase abstracta SortAlgorithm que utiliza los métodos ya definidos en ésta y únicamente proporciona un comportamiento específico cuando se hacen llamadas al método compare con cualquier par de cadenas de caracteres, devolviendo siempre -1 (lo que coincide con la implementación que ya habíamos hecho en SortAlgorithmConcreteTestSubclass, que ya no es necesaria y puede ser deshechada).

De esta forma, podemos hacer pruebas unitarias a nuestras clases abstractas aunque las implementaciones que existan estén en proyectos distintos o, sencillamente, no dispongamos de ellas, y lo podemos hacer con una implementación limpia que nos genera el mínimo código necesario para llevarlas a cabo gracias a Mockito.

Código fuente


Puedes descargar el código fuente completo comprimido en ZIP aquí. O, si lo prefieres, lo puedes descargar del repositorio en Github.

Nota: Al proyecto final se le han incorporado un par de implementaciones que completan el uso del patrón Template Method.