Como hacer nuestros test más legibles con Hamcrest

0
16659

Como hacer nuestros test más legibles con Hamcrest

0. Índice de contenidos.

1. Introducción

Una de las cosas que he aprendido en la XPWeek 2001, es a hacer más legibles nuestros test de forma muy sencilla utilizando Hamcrest.

Hamcrest es una librería que nos provee de una serie de matchers que podemos utilizar para escribir nuestros test con un lenguaje más cercano al natural de manera que se hace más sencillo comprender que están comprobando nuestros test. Y no solo en java, Hamcrest está portado a C++, Objective C, Python, Php y Erlang.

En este tutorial, vamos a ver como podemos mejorar nuestros test con muy poco esfuerzo utilizando la librería Hamcrest. Por un lado veremos como nuestro código se hace más legible y por otro, veremos como conseguir mensajes de error más descriptivos.

2. Entorno

  • Hardware
    • Mackbook Pro
      • Intel Core i7 2Ghz
      • 8GB RAM
      • 500GB HD
      • Sistema Operativo: Mac OS X (10.7.1)
  • Software
    • Eclipse Indigo
    • JUnit 4.10
    • Hamcrest 1.3.RC2

3. Configuración del entorno.

El primer paso es descargar la librería para hacer uso de ella en nuestros tests. Para ello modificamos el pom.xml agregando las dependencias y dejamos que Maven se encargue del trabajo sucio ;P

La primera dependencia es de JUnit, que nos facilitará la creación de los test a través de anotaciones. Es importante utilizar el artefacto junit-dep para que Maven descarge una versión de JUnit que no traiga Hamcrest. Las dos dependencias siguientes son las relacionadas con Hamcrest, la primera trae los matchers más básicos y la segunda dependencia trae una graaaaan cantidad de matchers.


     
    	junit
    	junit-dep
    	4.10
    	test
	
    
    
		org.hamcrest
		hamcrest-core
		1.3.RC2
		test
	
	
	
		org.hamcrest
		hamcrest-library
		1.3.RC2
		test
	

4. Ejemplos

Como veis, pasamos directamente a los ejemplos porque como ya decía en la introducción del tutorial, el uso de la librería es muy sencillo y seguro que en seguida le cogéis el truco ;).

La idea es mostrar primero como se hace solo con Junit y a continuación mostrar como se hace con Hamcrest. Quizás los test que se van a hacer no tengan mucho sentido, pero el objetivo del tutorial es ver como usar la librería. Una vez que hayáis visto los ejemplos, seguro que no os es dificil extrapolar los ejemplos a vuestro entorno de test :).

Siguiendo el símil del fútbol del tutorial de nuestro compañero Miguel, vamos a tener un equipo de fútbol con varios jugadores y vamos a realizar unos cuantos test sobre el equipo de futbol.

package com.adictosaltrabajo.tutoriales.hamcrestTutorial;

import java.util.ArrayList;
import java.util.List;

public class AutentiaFC {
	
	final List jugadores;
	
	public AutentiaFC() {
		jugadores = crearJugadores();
	}

	public List getJugadores() {
		return jugadores;
	}
	
	private List crearJugadores() {
		//Oculto la implementación porque no es el objetivo del tutorial
	}
}
package com.adictosaltrabajo.tutoriales.hamcrestTutorial;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

public class Jugador {
	
	private final int dorsal;
	
	private final Posicion posicion;
	
	public Jugador(int dorsal, Posicion posicion) {
		this.dorsal = dorsal;
		this.posicion = posicion;
	}
	
	public Posicion getPosicion() {
		return posicion;
	}
	
	public int getDorsal() {
		return dorsal;
	}

	@Override
	public int hashCode() {
		//Oculto la implementación porque no es el objetivo del tutorial
	}
	
	@Override
	public boolean equals(Object obj) {
		//Oculto la implementación porque no es el objetivo del tutorial
	}
	
	@Override
	public String toString() {
		//Oculto la implementación porque no es el objetivo del tutorial
	}
}	

NOTA: Como la clase Jugador va a formar parte de una colección, es buena práctica redefinir equals y hashCode.

Primer ejemplo

Vamos a ver un primer ejemplo muy sencillo.

private AutentiaFC equipo = new AutentiaFC();

@Test
public void primerTest() {
	// solo JUnit
	assertNotNull(equipo);
		
	// Hamcrest
	assertThat(equipo, is(not(nullValue())));
}

La primera diferencia es que en vez de usar los diferentes asserts de JUnit como pueden ser assertNotNull, assertTrue, assertEquals, etc, usamos assertThat (que es de JUnit). ¿Que conseguimos con esto?, conseguimos una primera mejora de legibilidad al leer el test. Con JUnit sería algo como «afirma no nulo equipo» y con Hamcrest «afirma que equipo es no valor nulo».

Vamos a explicar un poco más la línea de Hamcrest «assertThat(equipo, is(not(nullValue())));», La gracia está en el segundo parámetro de la función «asserThat» en donde hemos usado una serie de matchers de Hamcrest para crear nuestro «assert». El primer matcher es is, que solo se usa por temas de legibilidad, se podría haber escrito otro assert sin usar el matcher is con la misma funcionalidad. El segundo matcher es not que sirve para la negación lógica del resultado de evaluar la expresión que le pasamos como parámetro. El tercer y último matcher es nullValue que sirve para comprobar, como su nombre indica, si el valor es «null».

Otra mejora que obtenemos con el uso de Hamcrest es el mensaje descriptivo que nos devuelve en caso de que no se cumpla un assert. Si por ejemplo hacemos fallar el test anterior en el assert de JUnit y luego en el assert con Hamcrest obtenemos estas salidas:

Mensaje de error sin Hamcrest

Mensaje de error con Hamcrest

Sobre los mensajes de error, es cierto que a los assert de JUnit se le puede poner como primer parámetro un mensaje de error descriptivo, pero personalmente no me gusta esa opción, ya que si más tarde modificamos el test (aunque en principio una vez hecho un test, éste no se debería de modificar) además tendremos que modificar los comentarios. Y no solo por tener que mantener la documentación, si no que si por algún casual se nos olvida modificar el comentario del test y el test falla más adelante, puede ser muy difícil descubrir la causa del error, ya que el mensaje que nos estará mostrando no se correspondería con la realidad.

Segundo ejemplo

A continuación os muestro otro ejemplo de uso de Hamcrest

@Test
public void comprobarSiHayJugadores() {
	//solo JUnit
	assertFalse(equipo.getJugadores().isEmpty());
		
	// Hamcrest
	assertThat(equipo.getJugadores().isEmpty(), is(false));
		
	// otra opción, que hay que castear a Collection debido al uso de genéricos que hace Hamcrest
	// El mensaje de error es más descriptivo
	assertThat((Collection)equipo.getJugadores(), is(not(empty())));
}

Igual que en el caso anterior, mejoramos a la hora de leer el código. En el primer caso sería algo como: «afirma falso equipo dame jugadores es vacío». En el segundo caso sería algo como: «afirma que equipo dame jugadores es vacío es falso.» Quizás mi libre traducción 😛 no sea la más clara pero bueno, seguro que cuando leéis el código notáis que es algo más fácil de comprender.

Mensaje de error sin Hamcrest

Mensaje de error con Hamcrest

Mensaje de error con Hamcrest, 2º opción

Tercer ejemplo

El contenido del tercer test de ejemplo es el siguiente:

@Test
public void comprobarSiTenemosPortero() {
	final Jugador portero = new Jugador(1,Posicion.PORTERO);
		
	// solo junit
	assertTrue(equipo.getJugadores().contains(portero));
		
	// con Hamcrest
	assertThat(equipo.getJugadores(), hasItem(portero));
}

En este test estamos usando el matcher hasItem para comprobar si en nuestro equipo tenemos un portero, o lo que es lo mismo si en la lista de jugadores existe un item con la posición portero. La lectura con JUnit es algo como «afirma true equipo dame jugadores contiene portero». La lectura con Hamcrest es «afirma que equipo dame jugadores tiene item portero». Quizás en este caso, la mejora no sea mucha, pero ahora veremos como en el ejemplo siguiente como la legibilidad mejora cuando comprobamos con varios items.

Mensaje de error sin Hamcrest

Mensaje de error con Hamcrest

Como podemos ver en el mensaje de error con Hamcrest, se esperaba que en la colección de jugadores de AutentiaFc hubiese un entrenador, pero no lo hay 🙂

Cuarto ejemplo

@Test
public void comprobarSiHayPorteroYDefensaYDelantero() {
	final Jugador portero = new Jugador(1, Posicion.PORTERO);
	final Jugador defensa = new Jugador(3, Posicion.DEFENSA);
	final Jugador delantero = new Jugador(10, Posicion.DELANTERO);
		
	// solo junit
	assertTrue(equipo.getJugadores().contains(portero) && equipo.getJugadores().contains(defensa)
		&& equipo.getJugadores().contains(delantero));

	// con Hamcrest
	assertThat(equipo.getJugadores(), hasItems(portero, defensa, delantero));
}

En este caso, si se nota una mayor mejoría al usar Hamcrest. Si hacemos como en los casos anteriores e intentamos leer el código tenemos que el ejemplo de JUnit sería: «afirma true equipo dame jugadores contiene portero equipo dame jugadores contiene defensa equipo dame jugadores contiene delantero». En el segundo caso sería «afirma que equipo dame jugadores tiene items portero defensa delantero».

Mensaje de error sin Hamcrest

Mensaje de error con Hamcrest

En este caso hemos conseguido aumentar la legibilidad del código y obtener un mensaje más descriptivo en caso de que no se verifique el test.

Quinto ejemplo

@Test
public void comprobarSiHayPorteroYDefensaYDelantero() {
	final Jugador portero = new Jugador(1, Posicion.PORTERO);
	final Jugador defensa = new Jugador(3, Posicion.DEFENSA);
	final Jugador delantero = new Jugador(10, Posicion.DELANTERO);
		
	// solo junit
	assertTrue(equipo.getJugadores().contains(portero) && equipo.getJugadores().contains(defensa)
			&& equipo.getJugadores().contains(delantero));

	// con Hamcrest
	assertThat(equipo.getJugadores(), hasItems(portero, defensa, delantero));
}

En este caso, la única diferencia con el anterior es el uso del matcher anyOf, que lo podemos utilizar cuando que se cumpla alguna de las condiciones que le pasamos por parámetro. Es equivalente al operador boleano «||» de Java. También existe el matcher allOf que es el equivalente al operador boleano «&&».

5. Construir nuestro matcher

Existen una gran cantidad de matchers que podemos usar, sobre todo si hemos importado hamcrest-library, pero puede resultarnos interesante implementar el nuestro.

Hamcrest nos provee de una clase de la que heredar para construir nuestro matcher. La clase es TypeSafeMatcher, que nos obligará a implementar dos métodos: public boolean matchesSafely(T item) y public void describeTo(Description description);. En el primer método haremos las comprobaciones que necesitemos y en el segundo escribiremos el mensaje de error en caso de que no se cumpla el test.

Nuestro matcher va a recibir un equipo, en este caso AutentiaFC, que va a comprobar que el equipo sigue una formación 4-4-2.

La implementación quedaría así:

package com.adictosaltrabajo.tutoriales.hamcrestTutorial;

import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;

public class IsTacticaCuatroCuatroDosMatcher  extends TypeSafeMatcher {

	private int numPorteros = 0;
	private int numDefensas = 0;
	private int numCentrocampistas = 0;
	private int numDelanteros = 0;

	public void describeTo(Description description) {
		description.appendText("Expected 4-4-2\n");
		description.appendText("Got: ");
		description.appendText(numDefensas + "-" + numCentrocampistas + "-" + numDelanteros);
	}

	@Override
	public boolean matchesSafely(AutentiaFC item) {
		
		for(Jugador jugador : item.getJugadores()) {
			switch(jugador.getPosicion()) {
				case PORTERO:
					numPorteros++;
					break;
				case DEFENSA:
					numDefensas++;
					break;
				case CENTROCAMPISTA:
					numCentrocampistas++;
					break;
				case DELANTERO:
					numDelanteros++;
					break;
			}
		}
		return ((numPorteros == 1) && (numDefensas == 4)
				&& (numCentrocampistas == 4) && (numDelanteros == 2));
	}
	
	@Factory
	public static  Matcher tacticaCuatroCuatroDos() {
		return new IsTacticaCuatroCuatroDosMatcher();
	}
}

Además se ha añadido el método tacticaCuatroCuatroDos que nos construirá el matcher cuando lo necesitemos desde el test. La anotación @Factory sirve para que utilidades puedan reconocerlo. Hamcrest viene con una utilidad que reconoce estas anotaciones para que nos cree una única factoria con todos nuestros custom matchers de tal modo que sólo necesitemos un único import static en nuestro test.

Para poder usar nuestro matcher sólo tenemos que incluir el método de factoría. Una vez incluido, ya podemos usar nuestro matcher en un assertThat :D.

import static com.adictosaltrabajo.tutoriales.hamcrestTutorial.IsTacticaCuatroCuatroDosMatcher.tacticaCuatroCuatroDos;
@Test
public void comprobarConUnCustomMatcher() {
	assertThat(equipo, is(tacticaCuatroCuatroDos()));
}

Si provocamos el fallo quitando un delantero a nuestro equipo, tenemos un error como este 🙂



6. Conclusiones

Hemos visto como podemos hacer nuestros test más legibles con muy poco esfuerzo usando Hamcrest. Y si no nos fuese útil ningún matcher de la gran cantidad que trae, vemos que es muy sencillo crear los nuestros, de forma que se ajusten perfectamente a nuestras necesidades.

Para cualquier comentario, duda o sugerencia, tenéis el formulario que aparece a continuación.

Un saludo.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad