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

Robolectric (I): Unit Testing en Android

Al intentar aprender Android y cómo desarrollar software sobre su plataforma, lo primero que sorprende es lo complicado (y lento) que resulta hacer TDD. Por suerte, la gente de Pivotal Labs ha creado Robolectric , un framework que permite realizar Unit Tests sin disponer de un emulador Android activo. En primer lugar, debes disponer de un proyecto Android , generado desde Eclipse o desde Línea de Comandos . A partir del proyecto, y usando Maven , la instalación es inmediata. Simplemente hay que crear un fichero pom.xml en la raíz del proyecto, con las siguientes dependencias: [... Cabecera del Proyecto ...] <dependencies> <dependency> <groupId>com.google.android</groupId> <artifactId>android</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.pivotallabs</groupId> <artifactId>r