Matchers Hamcrest: seu teste mais legível!

Como você faz suas asserções nos seus testes de unidade? Eu, por exemplo, sou desenvolvedor Java e utilizo o JUnit para criar meus testes de unidade. Até bem pouco tempo utilizava apenas as asserções built-in do JUnit para validar meus testes: assertEquals, assertTrue, assertFalse, assertNotNull e outras existentes no pacote org.junit.Assert*. Isso até descobrir os matchers Hamcrest.

Não que as asserções do JUnit sejam ruim ou não atendam completamente – esse não é o problema.  Mas o Hamcrest fornece um conjunto mais rico – e completo – de asserções que fazem com que o código do teste torne-se mais expressivo e com melhor legibilidade.

 

Certo, mas o que é o Hamcrest?

 

O Hamcrest é um framework que possibilita a criação de regras de verificação (matchers) de forma declarativa. Como dito no próprio site do Hamcrest,

Matchers that can be combined to create flexible expressions of intent.

 

Portanto a ideia é que com os matchers Hamcrest as asserções utilizadas expressem melhor a sua intenção, ficando mais legíveis e mais expressivas.

Um matcher Hamcrest é um objeto que

  • reporta se um dado objeto satisfaz um determinado critério;
  • pode descrever este critério; e
  • é capaz de descrever porque um objeto não satisfaz um determinado critério.

Veja abaixo um exemplo de código que utiliza matchers Hamcrest para fazer algumas asserções verificando se uma dada string contém uma determinada substring.

 


String verso = "Minha terra tem palmeiras onde canta o sabiá.".
assertThat(verso, containsString("palmeiras"));
assertThat(verso, not(containsString("pinheiro")));

 

Observe que os matchers Hamcrest, na prática, são utilizados com o método assertThat() do JUnit. Esse método utiliza as funcionalidades auto-descritivas dos matchers para deixar claro exatamente o que ocorreu de errado em caso de uma asserção falhar. Assim se, para o código da listagem anterior, escrevermos a asserção seguinte,

 

assertThat(verso, not(containsString("palmeiras")));

 

haverá um erro na asserção com a seguinte saída:

java.lang.AssertionError: 
Expected: not a string containing "palmeiras"
     got: "Minha terra tem palmeiras onde canta o sabiá"

 

Ou seja, em vez de precisar escrever código que verifique explicitamente uma condição para gerar uma mensagem informativa de erro, podemos passar uma expressão matcher para o método assertThat() e deixá-lo fazer todo o trabalho.

 

Código de teste com matchers Hamcrest

 

Para mostrar a expressividade de uma asserção com o Hamcrest, vamos comparar dois códigos de testes de unidade: um sem o uso de asserções do Hamcrest e um outro com o uso destas asserções.

O código é simples e nos permite verificar atributos em elementos de uma coleção. O código completo desse exemplo está no meu repositório no GitHub. Para o exemplo, foi utilizado o JUnit 4 e o Hamcrest 1.3. Vamos então ao código.

 

A classe abaixo é um POJO simples de uma versão bastante simplificada de uma conta bancária. Esse POJO será utilizado em nossos exemplos dos testes de unidade.

public class ContaBancaria implements Comparable<ContaBancaria> {

	private String agencia;

	private String numero;

	private BigDecimal saldo;

	public ContaBancaria(String agencia, String numero, BigDecimal saldo) {
		super();
		this.agencia = agencia;
		this.numero = numero;
		this.saldo = saldo;
	}

	public String getAgencia() {
		return agencia;
	}

	public String getNumero() {
		return numero;
	}

	public BigDecimal getSaldo() {
		return saldo;
	}

	public int compareTo(ContaBancaria outra) {
		return this.getSaldo().compareTo(outra.getSaldo());
	}

}

Agora vamos mostrar um trecho de teste de unidade com o JUnit que faz algumas verificações em uma coleção de contas bancárias.

public class ContaBancariaTest {

	@Test
	public void verificaSaldoContasSemHamcrest() {

		ContaBancaria conta01 = new ContaBancaria("0001", "1234-1", new BigDecimal(1500.00));
		ContaBancaria conta02 = new ContaBancaria("0001", "1234-2", new BigDecimal(10000.00));
		ContaBancaria conta03 = new ContaBancaria("0001", "1234-3", new BigDecimal(5000.00));

		List<ContaBancaria> contas = 
                     Arrays.asList(new ContaBancaria[] { conta01, conta02, conta03 });

		BigDecimal saldoMinimo = new BigDecimal(1000.00);

		// verifica que todas as contas pertencem a mesma agência e que o saldo
		// de todas as contas é maior que o saldo mínimo
		for (ContaBancaria conta : contas) {
			assertEquals("0001", conta.getAgencia());
			assertTrue(conta.getSaldo().compareTo(saldoMinimo) == 1);
		}

	}

}

No código acima utilizamos um loop para verificar que todas as contas pertencem a mesma agência e que todas tem um valor de saldo maior que o valor mínimo (1000.00).

Abaixo, o mesmo código utilizando o asserções Hamcrest. Observe o uso em conjunto com o assertThat() do JUnit.

	@Test
	public void verificaSaldoContasComHamcrest() {

		ContaBancaria conta01 = new ContaBancaria("0001", "1234-1", new BigDecimal(1500.00));
		ContaBancaria conta02 = new ContaBancaria("0001", "1234-2", new BigDecimal(10000.00));
		ContaBancaria conta03 = new ContaBancaria("0001", "1234-3", new BigDecimal(5000.00));

		List<ContaBancaria> contas = 
                     Arrays.asList(new ContaBancaria[] { conta01, conta02, conta03 });

		BigDecimal saldoMinimo = new BigDecimal(1000.00);

		assertThat(contas, 
                    everyItem(Matchers.<ContaBancaria> hasProperty("agencia", equalTo("0001"))));
		assertThat(contas, 
                    everyItem(Matchers.<ContaBancaria> hasProperty("saldo", greaterThan(saldoMinimo))));

	}

As duas últimas linhas contém as mesmas asserções anteriores: verificar que todas as contas possuem a mesma agência e que todas as contas tem um saldo maior que o mínimo. A grande diferença é a expressividade do código:

  • Não é necessário escrever o loop que faz a iteração nas contas;
  • Os métodos do Hamcrest são bem mais descritivos (expressam bem sua intenção): everyItem(), hasProperty(), equalTo(), greaterThan(). A simples leitura deles já nos permite concluir o que está sendo feito. Bem diferente do trecho abaixo, onde temos que parar um pouco (além de saber o contrato de compareTo()) para entender o que está sendo feito:
		assertTrue(conta.getSaldo().compareTo(saldoMinimo) == 1);

Para fechar, um último exemplo que verifica se a lista de contas está ordenada de forma crescente pelo saldo das mesmas. Veja a simplicidade e expressividade da verificação feita com o método contains() do Hamcrest.

	@Test
	public void verificaOrdemCrescenteContas() {
		ContaBancaria conta01 = new ContaBancaria("0001", "1234-1", new BigDecimal(1500.00));
		ContaBancaria conta02 = new ContaBancaria("0001", "1234-2", new BigDecimal(10000.00));
		ContaBancaria conta03 = new ContaBancaria("0001", "1234-3", new BigDecimal(5000.00));

		List<ContaBancaria> contas = 
                        Arrays.asList(new ContaBancaria[] { conta01, conta02, conta03 });
		Collections.sort(contas);

		assertEquals(conta01, contas.get(0));
		assertEquals(conta03, contas.get(1));
		assertEquals(conta02, contas.get(2));
	}

	@Test
	public void verificaOrdemCrescenteContasComHamcrest() {
		ContaBancaria conta01 = new ContaBancaria("0001", "1234-1", new BigDecimal(1500.00));
		ContaBancaria conta02 = new ContaBancaria("0001", "1234-2", new BigDecimal(10000.00));
		ContaBancaria conta03 = new ContaBancaria("0001", "1234-3", new BigDecimal(5000.00));

		List<ContaBancaria> contas =
                        Arrays.asList(new ContaBancaria[] { conta01, conta02, conta03 });
		Collections.sort(contas);

		assertThat(contas, contains(conta01, conta03, conta02));
	}

Conclusão

 

O Hamcrest é um poderoso framework de matchers. A simplicidade e  a expressividade dos seus matchers o tornam uma excelente opção para uso em seus testes de unidade, sobretudo com o uso de Collections. Ainda é possível, também, criar matchers customizados, estendendo e ampliando o poder do framework. Mas esse é tema para um outro post. 😉

O Hamcrest, como vimos, integra-se muito bem com o JUnit e tem versões para várias linguagens: Java, Python, Ruby, Objective-C, PHP, Erlang e Swift.

Não esqueça de acessar o projeto no GitHub para ter acesso completo ao código.

Qualquer problema, dúvida ou sugestão é só falar.

 

Referências

Página oficial do Hamcrest: http://hamcrest.org/

The Hamcrest Tutorial

Growing Object-Oriented Software, Guided by Tests