Visión general
Hamcrest es un framework utilizado para las pruebas de tests unitarios. Permite escribir el conjunto de pruebas de forma declarativa basándose en objetos de coincidencia y reglas de coincidencia, los matchers. Viene distribuido con JUnit por lo que no es necesario una instalación o configuración extra.
En este tutorial trataremos de forma sencilla la funcionalidad de Hamcrest y aprenderemos cómo aprovecharla para escribir pruebas unitarias más nítidas e intuitivas en nuestro desarrollo de software.
¿Por qué utilizarlo?
La librería de Hamcrest dispone de una serie de matchers que podemos utilizar para escribir nuestros test con un lenguaje más cercano al natural. De esta manera se hace más sencillo comprender lo que están comprobando nuestros test.
La principal diferencia es que, en vez de usar los diferentes asserts de JUnit; como pueden ser assertNotNull, assertTrue, assertEquals, etc, usamos un único assert, assertThat, que es de JUnit. ¿Qué se consigue con esto?, se consigue una mejora de legibilidad al leer los test. Con JUnit sería algo como assert not null objeto
(asegura no nulo objeto) y con Hamcrest assert that objeto is not null
(asegura que objeto no es nulo).
Imposible nada es. Difícil, muchas cosas son.
Maestro Yoda
Por otra parte, cuando se escriben las pruebas, a veces es difícil encontrar el equilibrio adecuado entre la especificación excesiva de la prueba (hacerla frágil a los cambios), y no especificar lo suficiente (hacerla menos exigente y que continúe pasando, incluso cuando se rompe lo que se está probando).
Hamcrest permite seleccionar la precisión y flexibilidad del código bajo prueba. Esto ayuda mucho en la escritura de pruebas que pueden darse por «correctas». Dichas pruebas fallan cuando el comportamiento del aspecto bajo prueba se desvía del comportamiento esperado, pero continúan pasando cuando se realizan cambios menores y no relacionados en el comportamiento.
Reescritura del test con Hamcrest
En el artículo anterior de Testing con JUnit presentamos una estructura de ejemplo de un test realizado con las aserciones de JUnit. En este caso mostramos, a modo de comparación, dicho test y su homólogo utilizando el framework de Hamcrest. Como se puede observar, hemos pasado de definir los test al estilo del maestro Yoda a hacerlo de una forma más natural.
Test con asserts de JUnit
public class CalculadoraTest { @Test public void multiplicaPorCeroDebeRetornarCero() { Calculadora calcTester = new Calculadora(); assertEquals(0, calcTester.multiplica(10, 0), "10 x 0 debe ser 0"); } }
Test con assertThat de JUnit y matchers de Hamcrest
public class CalculadoraTest { @Test public void multiplicaPorCeroDebeRetornarCero() { Calculadora calcTester = new Calculadora(); assertThat("10 x 0 debe ser 0", calcTester.multiplica(10, 0), equalsTo(0)); } }
Matchers de Hamcrest
En este apartado se han agrupado los matchers más comunes de Hamcrest. Para más simplicidad se han utilizado objetos de tipo String, pudiendo ser de cualquier otro tipo. El conjunto de métodos a evaluar procede del paquete Core de Hamcrest, ya incluido por defecto en JUnit. Se recomienda revisar el Javadoc del proyecto para ver el resto de paquetes disponibles.
Base
is
Hamcrest se esfuerza por hacer que sus pruebas sean lo más legibles posible. Este matcher no agrega ningún comportamiento adicional al matcher. Las siguientes afirmaciones son equivalentes:
@Test public void isExample() { assertThat ("foo", is("foo")); assertThat ("foo", equalTo("foo")); assertThat ("foo", is (equalTo("foo"))); }
not
Da la prueba por válida si los objetos no coinciden.
@Test public void notExample() { assertThat("foo", is(not("bar"))); }
Objetos
is – equalTo
Compara ambos objetos mediante el método Object.equals()
. En el caso especial de pasar un Array este matcher dará positivo si ambos arrays tienen la misma longitud y objetos lógicamente coincidentes en las mismas posiciones. is(Tipo x)
es una abreviatura para is(equalTo(Tipo x))
.
@Test public void equalToExample() { assertThat("foo", equalTo("foo")); // Distinto objeto pero coincide en longitud e items assertThat(new String[] {"foo", "bar"}, is(equalTo(new String[] {"foo", "bar"}))); //El orden no es el mismo! assertThat(new String[] {"bar", "foo"}, is(not(equalTo(new String[] {"foo", "bar"})))); }
sameInstance
Comprueba si ambos objetos comparados son la misma instancia.
@Test public void sameInstanceExample() { String foo = "foo"; String sameFoo = foo; assertThat(foo, is(sameInstance(sameFoo))); }
isA – instanceOf
Compara el tipo de objeto mediante el método Class.isInstance(Object). El matcher creado no asume ninguna relación entre el tipo especificado y el objeto examinado. isA(Clazz.class) es una abreviatura para is(instanceOf(Clazz.class)).
@Test public void instanceOfExample() { assertThat("foo", is(not(instanceOf(Integer.class)))); assertThat("foo", isA(String.class)); }
any
Compara el tipo de objeto mediante el método Class.isInstance(Object). El matcher creado fuerza una relación entre el tipo especificado y el objeto examinado.
@Test public void anyExample() { // No coincide el tipo! //assertThat("foo", is(not(any(Integer.class)))); assertThat("foo", is(any(String.class))); }
Diferencias entre «instanceOf» y «any»
<T> Matcher<T> instanceOf(java.lang.Class<?> type)
Al método le da igual los tipos, en nuestro ejemplo comparamos el tipo String con el tipo Integer.
<T> Matcher<T> any(java.lang.Class<T> type)
El tipo debe de coincidir, el método espera que sean el mismo tipo. Debemos comparar el tipo String con un objeto que satisfaga el tipo String, obligatoriamente.
Cadenas
startsWith
Crea un matcher que evalúa si la cadena examinada comienza con la subcadena pasada.
@Test public void startsWithExample() { assertThat("foo bar baz", startsWith("foo")); }
endsWith
Crea un matcher que evalúa si la cadena examinada termina con la subcadena pasada.
@Test public void endsWithExample() { assertThat("foo bar baz", endsWith("baz")); }
containsString
Crea un matcher que busca la subcadena pasada en cualquier posición de la cadena.
@Test public void constainsStringExample() { assertThat("foo bar baz", containsString("bar")); }
Booleanos
allOf
Da la prueba por válida si todos los matchers que engloba coinciden. Similar al operador Java && en cortocircuito.
@Test public void allOfExample() { assertThat("foo bar baz", allOf(startsWith("foo"), endsWith("baz"))); }
anyOf
Da la prueba por válida si alguno de los matchers que engloba coincide. Similar al operador Java || en cortocircuito.
@Test public void anyOfExample() { assertThat("foo bar qux", anyOf(startsWith("foo"), endsWith("baz"))); }
both
Crea un matcher que da la prueba por válida si se cumplen ambas condiciones.
@Test public void bothExample() { assertThat("foo bar baz", both(startsWith("foo")).and(endsWith("baz"))); }
either
Crea un matcher que da la prueba por válida si se cumple al menos una de las condiciones.
@Test public void eitherExample() { assertThat("foo bar baz", either(startsWith("foo")).or(endsWith("baz"))); }
Colecciones
hasItem
Crea un matcher para elementos Iterables. Realiza un único pase con el elemento a buscar. Se detiene al momento de encontrar la primera ocurrencia.
@Test public void hasItemExample() { assertThat(Arrays.asList("foo", "bar", "baz"), hasItem("bar")); assertThat(Arrays.asList("foo", "bar", "baz"), hasItem(startsWith("b"))); }
hasItems
Crea un matcher para elementos Iterables. Realiza tantos pases como elementos a buscar. Se detiene al momento de encontrar la primera ocurrencia.
@Test public void hasItemsExample() { assertThat(Arrays.asList("foo", "bar", "baz"), hasItems("bar", "baz")); assertThat(Arrays.asList("foo", "bar", "baz"), hasItems(endsWith("o"), endsWith("z"))); }
Hamcrest con JUnit
Descarga el código completo de los ejemplos mostrados.