Test unitarios con fechas en Java

Introducción

En el artículo anterior hablé de forma general de como comencé a crear test unitarios en Java.
En este quiero hablar en específico de crear test unitarios cuando el código a testear utiliza fechas.

Manejar fechas en Java es complicado (al menos hasta la versión 7) pero es incluso peor el tema cuando intentas crear test unitarios para tu código.

El problema principal de manejar fechas en Java es debido a que la clase Date, que es la que se suele utilizar habitualmente, es mutable. Es decir, que se puede modificar su valor interno después de haber sido inicializada.
El otro problema es el como crear test unitario con fechas si utiliza como referencia la fecha actual del sistema.

Contexto

Como en el artículo anterior he utilizado en el código las librerias Junit v4 y harmcrest. Podéis encontrarlas en:

http://junit.org/
http://hamcrest.org/JavaHamcrest/

Mutable?¿

Te preguntarás cual es el problema que los objectos de tipo Date sean mutables.
El tema es que podemos modificar el campo fecha de un objeto sin que este pueda hacer nada.

Vamos a verlo en un ejemplo. Tenemos un clase User con un campo con la fecha de nacimiento de tipo Date:

En el siguiente código podemos ver en un test unitario cómo puede afectar la mutabilidad a la clase User:

Hemos cambiado el año de la fecha de nacimiento del usuario de 1990 a 2014 sin que el objeto user lo sepa.

Formas de probar código con fechas

Hay principalmente tres formas de crear métodos testeables que contengan fechas:

  • 1. Pasar la fecha actual como parámetro. De esta forma resulta muy fácil el cambiar la fecha actual del sistema y así probar todos los posible casos que queramos.
  • 2. Obtener la fecha actual por un método que podamos cambiar. Dentro del método que queremos probar, obtenemos la fecha actual del sistema por un método (en la misma clase) con el scope protected para que en el test unitario podamos sobrescribir el método utilizando una clase que extienda ese método.
  • 3. Utilizar nuestra propia clase como fecha del sistema. Dentro del método que queremos probar, utilizamos una clase propia para obtener una instancia de la fecha actual del sistema.

Creo que la mejor forma de ver como funcionan estas tres formas de probar código con fechas es utilizando el mismo problema y ver como es el código en las tres aproximaciones.

El problema: ¿es mi cumpleaños hoy?

Tenemos la misma clase User que utilizamos al principio del artículo:

y queremos probar el método isBirthday (no está implementado en el código anterior porque es diferente según la aproximación). Esté método comprueba si el usuario cumple años hoy y devuelve un boolean con el resultado.

Si para obtener la fecha actual del sistema utilizamos:

new Date()

El código no sería testable porque no podríamos cambiarla.
Nota: Esto es solo un ejemplo de como probar código con fechas. Personalmente no suelo probar cada método de forma individual sino que pruebo el comportamiento del sistema.

Primera aproximación: pasar la fecha actual como parámetro

Primeramente veremos la estructura del método a probar y posteriormente el código de los test unitarios. Así el método isBirthday sería como aparece a continuación:

La fecha actual (now) se ha pasado como parámetro, luego se comprueba los dias y meses de los objetos now y dayOfBirthday de tipo Date (se ha omitido esa parte por innecesaria en el ejemplo) y posteriormente se devolverá true o false según estas comprobaciones.

A continuación dos test unitarios que comprueban los dos casos básicos del método, que el usuario cumple años hoy y que no cumple años hoy:

Las variables five_july_2000, six_july_2010 y five_july_2010 son constantes de tipo Date que como no aportan nada al ejemplo he eliminado su definición.

 

Segunda aproximación: obtener la fecha actual con un método que podamos cambiar

Seguimos con el mismo problema pero como obtenemos la fecha actual de un método no necesitamos pasarlo como parámetro. Así, el código del código de producción es:

Los mismos dos test unitarios de la aproximación anterior son implementados en el siguiente código con el nuevo método:

Lo que estamos haciendo en ambos test unitarios es crear una clase anónima que extiende de la clase User, donde está sobrescrito el método getDate con la fecha actual que deseamos.

 

Tercera y última aproximación: Nuestra propia clase fecha del sistema

Esta tercera forma de probar código con fechas la leí en el libro Test Driven de Lasse Koskela y es la que suelo utilizar habitualmente.
Podéis encontrar el código fuente de esta junto con todo el código fuente del libro en la siguiente dirección:

http://www.manning.com/koskela/

El código de producción sería:

La clase SystemTime que hemos utilizado para obtener la fecha actual, es parte de esta tercera aproximación, y consiste en una clase que genera la fecha actual a partir del valor en milisegundo de una instancia hija de la interfaz TimeSource:

que por defecto tiene el valor System.currentTimeMillis() pero que puede ser modificada si le pasamos a SystemTime otra instance hija que implemente la interfaz TimeSource.

El código que nos interesa de la clase SystemTime es:

Así los dos test unitarios quedarían de la siguiente forma:

Como puedes comprobar aunque los test quedan bastante simples, pueden afectar al comportamiento de otros test unitarios que utilicen la clase SistemTime para obtener la fecha actual ya que no será la real.

Para solucionarlo tenemos que asignar a SystemTime su valor por defecto. Para ello, en el método que se ejecutar después de cada test unitario lo añadimos:

 

Ejemplo de donde utilizar estos métodos

Hay un caso en particular que me está viniendo muy bien el poder cambiar la fecha del sistema, y es cuando pruebo procesos en segundo plano.

En general los procesos en segundo plano siempre tienen un campo de tipo Date que la forma de probar que funciona es manualmente: esperar hasta cierto día/hora en concreto para comprobar que ha funcionado como debía.
Si podemos crear test unitarios cambiando la fecha del sistema tendremos más seguridad que nuestro proceso funciona.
Esto no quita que se pueda probar manualmente que el proceso funciona.

 

Conclusiones

Aunque personalmente la aproximación que más me gusta es la última creo que mientras el código sea testeable cualquiera vale.

Modificado 08-01-2017: Cambiado formateo del código a GitHub

  • Muy completo el post Enrique, te felicito 🙂

  • kikers25

    Muchas gracias, Carlos 🙂

  • Andrés Viedma

    A mi también me gusta más la última opción 🙂

    No sé si conoces este artículo de JB Rainsberger que sobre este mismo ejemplo de las fechas le da todavía alguna vuelta de tuerca más, está muy bien:

    http://blog.thecodewhisperer.com/2013/11/23/beyond-mock-objects/

  • Enrique Martín

    Lo leí hace un tiempo y no lo entendí del todo. ¿Cuales crees que son las conclusiones?

  • Andrés Viedma

    Bueno, propone mover lo más arriba posible dentro del árbol de dependencias todos los elementos que dependan del contexto, como por ejemplo la fecha actual del sistema. Él en su ejemplo mueve la obtención de la fecha actual a la capa del controlador. Luego va más allá y crea una clase controladora aposta para manejar estas cosas, y hacer que todo tu código sea parametrizable fácilmente por el instante actual y no dependa de tener que crear un SystemTime o similares. Es una forma muy completita de plantear tu primera aproximación que me parece bastante interesante.