Ir al contenido principal

"Mocking the Time": Probando métodos que dependen de la fecha

En todos los proyectos aparecen métodos que necesitan realizar comprobaciones de fechas u horas en la lógica de negocio, ya sea consultando la hora actual del sistema, ya sea comparando rangos horarios.

Si antes de implementar el método le damos un pensada a cómo probarlo, aparece una duda importante: ¿cómo puedo suplantar la fecha o la hora para que mis tests no dependan del día o la hora actuales?

Pongamos un ejemplo sencillo: tenemos un método que si lo ejecutamos en un día par, tiene que llamar a un servicio externo. Una primera implementación, sin pensar cómo probarlo, nos daría un código parecido a:

[...]

public class TimeService {

    private IExternalService externalService = null;

    public void timeMethod() {
        Calendar cal = Calendar.getInstance();
        if (cal.get(Calendar.DAY_OF_MONTH) % 2 == 0) {
            externalService.externalMethod();
        }
    }

    [...]
}

Un código sencillo... ¡que resulta imposible de probar correctamente! Como se puede ver a continuación, si el test se ejecuta en un día par, funcionará. Pero si el test se ejecuta un día impar (situación corriente en un sistema de integración continua), fallará:

[...]

public class TimeServiceTest {
    
    [...]

    // ¡¡Este test sólo funciona los días pares!!
    @Test
    public void testTimeServiceShouldCallExternalServiceIfDayIsEven() {
        mockery.checking(new Expectations() {
            {
                exactly(1).of(externalServiceMock).externalMethod();
            }
        });

        timeService.timeMethod();
    }

    [...]

}

Llegados a este punto, a uno se le pueden ocurrir soluciones del estilo: "cambiemos la fecha del sistema"... pero no son muy recomendables. Yo prefiero aprovechar las bondades de la inyección de dependencias, que nos facilitará enormemente la tarea de probar el código.

Una posible solución es crear una clase TimeResolver (y opcionalmente su interfaz ITimeResolver) y utilizarla desde nuestros servicios para consultar la fecha actual:

package utils;

import java.util.Calendar;
import java.util.Date;

public class TimeResolver implements ITimeResolver {

    @Override
    public Calendar getCurrentCalendar() {
        return Calendar.getInstance();
    }

    @Override
    public Date getCurrentDate() {
        return new Date();
    }

}

Gracias a esta ayuda, ya podemos crear un test que simule lo que sucede en cualquier combinación de fechas y horas:

[...]

public class TimeServiceTest {

    private TimeService timeService = null;

    private Mockery mockery = new Mockery();
    private IExternalService externalServiceMock = null;
    private ITimeResolver timeResolverMock = null;

    @Before
    public void setUp() throws Exception {
        timeService = new TimeService();

        externalServiceMock = mockery.mock(IExternalService.class);
        timeResolverMock = mockery.mock(ITimeResolver.class);

        timeService.setExternalService(externalServiceMock);
        timeService.setTimeResolver(timeResolverMock);
    }

    @After
    public void tearDown() throws Exception {
        mockery.assertIsSatisfied();
    }

    @Test
    public void testTimeServiceShouldCallExternalServiceIfDayIsEven() {
        mockery.checking(new Expectations() {
            {
                Calendar cal = Calendar.getInstance();
                cal.set(Calendar.DAY_OF_MONTH, 8); // Even Day
                exactly(1).of(timeResolverMock).getCurrentCalendar();
                will(returnValue(cal));

                exactly(1).of(externalServiceMock).externalMethod();
            }
        });

        timeService.timeMethod();
    }

    @Test
    public void testTimeServiceShouldNotCallExternalServiceIfDayIsOdd() {
        mockery.checking(new Expectations() {
            {
                Calendar cal = Calendar.getInstance();
                cal.set(Calendar.DAY_OF_MONTH, 7); // Odd Day
                exactly(1).of(timeResolverMock).getCurrentCalendar();
                will(returnValue(cal));

                // External Method will not be called
                exactly(0).of(externalServiceMock).externalMethod();
            }
        });

        timeService.timeMethod();
    }

}

Este par de casos de test servirán para validar que el nuevo código se comporta como esperamos:

[...]

public class TimeService {

    private IExternalService externalService;
    private ITimeResolver timeResolver;

    public void timeMethod() {
        Calendar cal = timeResolver.getCurrentCalendar();
        if (cal.get(Calendar.DAY_OF_MONTH) % 2 == 0) {
            externalService.externalMethod();
        }
    }

    [...]

}

¡Conseguido! Ya podemos asegurar el correcto comportamiento de todos los casos que queramos. Sin duda, esta pequeña indirección me ha sido muy útil siempre que he necesitado trabajar con fechas o calendarios. Espero que os sea de utilidad.

Comentarios

Entradas populares de este blog

Dependency Injection de HttpClient 3.x

Para un fanático de la Dependency Injection como yo, siempre me ha costado trabajar con Apache HttpClient 3.x y la multitud de maneras que tiene la gente de inicializar los clientes. Además, como usuario asiduo de Spring Framework , es imprescindible disponer de una manera de cargar clientes HTTP desde el contexto XML y que sea sencilla de probar. Una de las soluciones más comunes para crear y configurar los clientes es la declaración de un método init() donde se inicializan todos los parámetros, obligando que nuestra clase conozca todos los detalles y parámetros de este componente externo. Pese a que la solución funciona, no deja de ser complicada de testear, y esto es justamente lo que se quiere evitar desde el Test Driven Development (TDD). Para ello, hay que buscar una fórmula que permita "inyectar" los clientes HTTP, ya configurados en el exterior, permitiendo a la clase de destino centrarse en su objetivo. En primer lugar, hay que conseguir la biblioteca HttpClie

Unit Tests (I): "mocking" la interacción

Indispensables. De entre todos los tipos de tests, son los más sencillos de escribir, aunque son los que permiten encontrar (a tiempo) la mayor cantidad de bugs. ¿Qué es un Unit Test? Mejor contar qué hacen y no lo que son: Un Unit Test debe probar un ÚNICO método de una ÚNICA clase . Simple. Sin excepciones (*) . El método a probar SIEMPRE debe tener un estado inicial conocido y un resultado esperado. ¡Siempre! Si cumple ambas premisas, el test podrá ser ejecutado de forma automática y sin depender de nadie (**) . Considero que probar la interacción de una clase con el resto de componentes es uno de los sistemas que mejores resultados ofrece. Estas interacciones se "simulan" en el mismo test, utilizando mock-objects. Antes de empezar con algún ejemplo de Unit Test con Mock Objects, hay algunos consejos que nos pueden facilitar el trabajo: Cada clase debe tener su Interfaz (***) , exceptuando las java.lang.Exception y poco más. Conocer el patrón de Inyección de Depen

Unit Tests (II): clases sin interfaz

Sigamos con los tests unitarios: ¿qué ocurre si alguna de mis dependencias no tiene interfaz? O simplemente, ¿qué ocurre si no me gusta añadir una interfaz a cada una de mis clases? Aunque recomiendo encarecidamente el uso de interfaces (ayudan a definir y delimitar el alcance de una clase), no es del todo extraño que algunas de nuestras dependencias no las tengan. Como ejemplo, veamos una que seguro que a muchos nos ha tocado implementar: un cliente para leer los mensajes de Twitter . Si decidimos usar el proyecto Twitter4J , nos encontramos con que su cliente twitter4j.Twitter no tiene interfaz. Si intentáramos hacer un test usando la guía del post anterior : [...] private TwitterService twitterService = null; private Mockery mockery = new Mockery(); private Twitter twitterMock = null; @Before public void setUp() { twitterService = new TwitterService(); twitterMock = mockery.mock(Twitter.class); twitterService.setTwitter(twitte