Ir al contenido principal

Matchers y jMock

A medida que la complejidad de nuestro código aumenta, también tiende a aumentar la complejidad de los tests. Al principio nos va bien con asegurarnos que la salida del método es la esperada. Pero llegado el momento en que necesitamos comprobar qué valores contiene cada campo, o qué información se utiliza para llamar a las otras clases, es hora de empezar a utilizar Matchers.

Los Matchers nos permiten especificar las restricciones o condiciones que deben cumplir los parámetros de entrada. En anteriores posts, cada vez que escribíamos with(any(String.class)), estábamos utilizando un Matcher (uno muy genérico, que permite cualquier cadena como parámetro de entrada).

Imaginemos que queremos asegurarnos que nuestro método getWeatherForecast() invoca al método getWeather() con una instancia de país, cuyo nombre debe ser el mismo que el recibido. Para ello vamos a utilizar el método hasProperty() de la clase org.hamcrest.Matchers:

[...]

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import org.hamcrest.Matcher;
import org.jmock.*;
import org.junit.*;

// Omito el resto de imports internos

public class BombServiceWithMatchersTest {

    // Omito la declaración de dependencias/mocks y los métodos setUp() y tearDown()

    @Test
    public void testGetWeatherShouldUseTheSameCountry() throws Exception {
        mockery.checking(new Expectations() {
            {
                Matcher<Country> matcher = hasProperty("name", equalTo("Avalon"));

                exactly(1).of(bombSoapClientMock).getWeather(with(matcher));
                will(returnValue(1));
            }
        });

        int result = bombService.getWeatherForecast("Avalon");
        assertEquals(1, result);
    }

    // ... other tests
}

Ahora tenemos un test que se asegura que nuestro servicio enviará siempre el mismo parámetro y, si en algún momento lo modificamos por error, el test nos avisará.

Como véis, es muy sencillo definir restricciones a los parámetros que utilizamos. Simplemente hay que tener en cuenta algunos detalles, que dependen del Matcher utilizado. Por ejemplo, el método hasProperty() requiere que la clase que estás evaluando (en mi caso, Country) cumpla las convenciones de Sun sobre getters/setters. Si en la clase Country no existieran los métodos getName() y setName(), el test no habría funcionado (y ¡cuidado!, porque este tipo de errores sólo informan que el método no se ha invocado, y no el porqué).

Para completar la explicación del ejemplo, hay que fijarse que el segundo parámetro del método hasProperty() es, a su vez, un Matcher. Allí podríamos definir cualquier restricción que afectara al valor del campo (en este caso forzamos que sea el valor conocido).

Con los Matchers se pueden conseguir unos tests muy completos, y que nos permitan detectar fácilmente si algún cambio en nuestro código rompe con el comportamiento esperado. Os recomiendo mirar todos los métodos disponibles de la clase org.hamcrest.Matchers, entre los que destacan:
  • hasEntry(): muy útil al trabajar con Maps, ya que nos permite definir restricciones sobre claves y valores.
  • hasKey(): comprueba si un Map contiene la clave facilitada.
  • hasItem(): comprueba si el elemento existe dentro de una List.
  • startsWith(): muy interesante al trabajar con Strings.
  • ...y muchos más que nos pueden ayudar en cualquier momento...

Por último, para realizar más de una comprobación por parámetro disponemos del método allOf(). Por ejemplo:
allOf(hasKey("key1"), hasKey("key2"));.

Sin embargo, como la clase Matcher utiliza Generics de forma intensa, os puede dar muchos problemas de compilación, intentando encajar los tipos de datos. Por ejemplo, el siguiente código no compila, ya que se espera un Matcher<Object> en vez de un Matcher<Country> (dichosas reglas de los Generics, que van buscando el "mínimo común múltiplo"):

Matcher<Country> matcher = allOf(
        hasProperty("name", equalTo("Avalon")),
        hasProperty("id", equalTo("1")));
exactly(1).of(bombSoapClientMock).getWeather(with(matcher));

La única solución que he encontrado es definir cada Matcher por separado, para que los tipos de cada variable se vayan acomodando por pasos, en vez de intentar inferirlos de golpe:

Matcher<Country> m1 = hasProperty("name", equalTo("Avalon"));
Matcher<Country> m2 = hasProperty("id", equalTo("1"));
Matcher<Country> matcher = allOf(m1, m2);
exactly(1).of(bombSoapClientMock).getWeather(with(matcher));

Espero que los Matchers os sirvan de ayuda para definir tests mucho más específicos.

Comentarios

Artanis ha dicho que…
Esto si que no me lo esperaba!!!
El señor Edrai despues de todo lo que machacaba a los pobres testers de Telefonica I+D (como servidor...) haciendo un blog sobre testing!!!
Que te has pasado al lado oscuro del desarrollo? :p
Edraí Brosa ha dicho que…
¡Toni! ¡Cuánto tiempo! :)

Tranquilo, que no me he pasado al lado oscuro... en este blog hablaré poquito de Acceptance Testing o parecidos (lo que os machacaba, vaya... jejeje), sino de Unit e Integration Testing.

Ya sabes: development sin tests no es development ;)

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