Ir al contenido principal

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:

Para los siguientes ejemplos utilizaré Java 6, JUnit 4.7 y JMock 2.5.1, aunque debería funcionar para cualquier versión Java 5+, JUnit 4+ y JMock 2+.

Utilizando un ejemplo parecido a los de Marc, imaginemos que tenemos que implementar el siguiente método:
public Interface IBombService {

  /**
   * Check if the country with countryName should be bombed.
   *
   * @return true if the country should be bombed, or false if not.
   *
   * @throws NoSuchElementException If no country with the given name exists.
   */
  public boolean shouldBeBombed(String countryName) throws NoSuchElementException;

}

Sabemos que el método deberá consultar si el nombre del país es válido. Luego deberá mirar el último ataque en nuestra base de datos, por si es demasiado pronto para volver a ser bombardeado. Finalmente deberá consultar un servicio web externo para verificar si debe ser atacado o no. Nada de HelloWorld... un caso más cercano a "nuestra" realidad.

La estructura que recomiendo usar para cualquier test unitario con JMock es la siguiente:

package cat.edra.impl;

import org.jmock.*;
import org.junit.*;

/**
 * Unit test for YetAnotherService methods.
 */
public class YetAnotherServiceTest {

    private YetAnotherService yetAnotherService = null;

    private Mockery mockery = new Mockery();

    // Interfaces de las dependencias del servicio

    @Before
    public void setUp() {
        yetAnotherService = new YetAnotherService();

        // Inicialización de cada uno de los mocks de las dependencias
        // Inyección de cada una de las dependencias "mocked".

        // Mock conditions for every test...
        mockery.checking(new Expectations() {
            {
            // Expectations que deben cumplir TODOS los tests (cache, recuperar un objeto, ...)
            }
        });
    }

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

    @Test
    public void testYetAnotherServiceCorrectFlow() throws Exception {
        mockery.checking(new Expectations() {
            {
                // Lista de Expectations que debe cumplir este test concreto.
            }
        });

        yetAnotherService.methodToTest(...);
    }

    @Test(expected = TheExpectedThrownException.class)
    public void testYetAnotherServiceIfAnExceptionIsRaised() throws Exception {
    [...]

}

En este punto, y aplicando algo de TDD, sería bueno plantearse los casos de prueba. Podríamos empezar implementando algunos de los siguientes:

public void testShouldBeBombedIfCountryNameNotExists()...
public void testShouldBeBombedIfDbException()...
public void testShouldBeBombedIfExternalServiceException()...
public void testShouldBeBombedIfExternalResponseIsTrue()...
public void testShouldBeBombedIfExternalResponseIsFalse()...

El límite está en nuestra imaginación y lo completa que sea nuestra implementación. Utilizar TDD al 100% nos permite implementar SÓLO el código que haga funcionar estos tests. Ni una línea más. Utilizar TDD a medias, nos permitirá ir añadiendo, poco a poco, nuevos casos de tests, aún a riesgo de implementar más líneas de código de las realmente necesarias. Sea como sea, los tests unitarios nos ayudarán (y mucho) a mejorar la calidad de nuestro código.

Como ejemplo, veamos la implementación con mock-objects de uno de los casos correctos y de un caso de error con la base de datos:

    [...]

    private BombService bombService = null;

    @Before
    public void setUp() {
        bombService = new BombService();

        countryDaoMock = mockery.mock(ICountryDao.class);
        bombDaoMock = mockery.mock(IBombDao.class);
        bombSoapClientMock = mockery.mock(BombSoapClient.class);

        bombService.setCountryDao(countryDaoMock);
        bombService.setBombDao(bombDaoMock);
        bombService.setBombSoapClient(bombSoapClientMock);
    }

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

    @Test
    public void testShouldBeBombed() throws Exception {
        mockery.checking(new Expectations() {
            {
                exactly(1).of(countryDaoMock).findCountryByName(with("Avalon"));
                will(returnValue(new Country("1", "Avalon", "AVA")));

                Calendar cal = Calendar.getInstance();
                cal.set(2008, 0, 1);
                exactly(1).of(bombDaoMock).getLastBombed(
                        with(any(Country.class)));
                will(returnValue(cal.getTime()));

                exactly(1).of(bombSoapClientMock).isPendingAttack(
                        with("Avalon"));
                will(returnValue(true));
            }
        });

        boolean result = bombService.shouldBeBombed("Avalon");
        assertTrue(result);
    }

    @Test(expected = NoSuchElementException.class)
    public void testShouldBeBombedIfCountryNameNotExists() throws Exception {
        mockery.checking(new Expectations() {
            {
                exactly(1).of(countryDaoMock).findCountryByName(with("Avalon"));
                will(returnValue(null));
            }
        });

        bombService.shouldBeBombed("Moon");
    }

    [...]


Una vez uno se acostumbra a la sintaxis de JMock (o equivalentes), es muy sencillo realizar tests unitarios que validen el comportamiento de nuestro método ante cada excepción y respuesta posible de sus dependencias.

La clase que estamos probando utiliza directamente su implementación private BombService bombService = null; en lugar de su interfaz. Cada test se encarga de reiniciar el estado original del objeto en el método setUp() y validar que todo finaliza correctamente en el tearDown(). El resto de dependencias sólo utilizan su interfaz, que se convertirá en el contrato que deberán seguir las implementaciones para ser compatibles con nuestro código (tanto los casos correctos, como los errores).

La única premisa para implementar estos tests es utilizar (o crear) las interfaces de cada una de las dependencias, con los métodos que pueden ser invocados desde el nuestro. Eso sí, sin implementación, ya que el comportamiento que esperamos de ellas se define en las Expectations.

Llegados a este punto, quedan muchos temas abiertos, que irán apareciendo en próximas entregas:
  • ¿Qué ocurre si alguna de mis dependencias no tiene interfaz?
  • ¿Cómo se prueban capas de acceso a datos o servicios web, que no tienen un estado inicial conocido? 
  • ¿Qué hacer si las Expectations de un test crecen desmesuradamente?
  • ...
Pero esto ya será carne de otros tutoriales... Mientras tanto, let's test! :)

(*) Aunque puedan existir casos justificados de tests unitarios contra más de una clase o método, no es ni normal ni recomendable que abunden en nuestro código.
(**) En próximos posts contaré qué pasa con los tests que no son fácilmente automatizables (como los que acceden a base de datos o sistemas de colas).
(***) Sólo es aconsejable dejar sin interfaz aquellas clases de poca entidad, como algún que otro Helper o similares.

Comentarios

Unknown ha dicho que…
Bona entrada.

M'ha agradat l'article, tot i que sóc una mica escèptic a tenir una interface per cada classe. Almenys a mi em resulta bastant molest pel que fa a la "navegabilitat" del codi.

He de suposar que fas la Dependency Injection amb Spring oi ? jo he estat utilitzant Guice, de Google. Però justament fa una estona m'he trobat amb el cas que he de fer IoC en una configuració de base de dades. M'ha portat estona perquè Guice no té cap solució bona per això. Spring + Hibernate hauria sigut millor en aquest cas.

A veure si fas un article Guice vs Spring :)
Edraí Brosa ha dicho que…
Gràcies Marc :)

Sí que tanta interfície fa que el codi sigui poc navegable, però moltes funcionalitats de frameworks com Spring o Guice les necessiten. I un cop t'acostumes, tampoc és tant estrany!

Ja fa temps que estic 100% Spring, i el tema de configurar la connexió a la DB està molt ben pensat (encara que sigui utilitzant JdbcTemplate a pèl, sense Hibernate o altres ORMs). I si ho barreges amb una mica de JNDI, tot és màgic!

Un dia d'aquests ens haurem de sentar a comparar-los... a veure què en traiem :)

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 (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