Dicas para Testes Unitários com AssertJ

Teste de unidade tornou-se uma parte padrão do desenvolvimento. Existem muitas ferramentas que podem ser utilizadas de diversas maneiras. Este artigo demonstra algumas dicas ou, digamos, melhores práticas que funcionam bem para mim.

Neste Artigo, Você Aprenderá

Não Exagere nas Verificações de NPE

Todos tendemos a evitar NullPointerException tanto quanto possível no código principal porque pode levar a consequências desagradáveis. Acredito que nossa principal preocupação não é evitar NPE nos testes. Nosso objetivo é verificar o comportamento do componente testado de uma maneira limpa, legível e confiável.

Má Prática

Muitas vezes no passado, usei a afirmação isNotNull mesmo quando não era necessário, como no exemplo abaixo:

Java

 

@Test
public void getMessage() {
	assertThat(service).isNotNull();
	assertThat(service.getMessage()).isEqualTo("Hello world!");
}

Este teste produz erros assim:

Plain Text

 

java.lang.AssertionError: 
Expecting actual not to be null
	at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)

Boa Prática

Embora a afirmação adicional isNotNull não seja realmente prejudicial, deve ser evitada por causa dos seguintes motivos:

  • Não adiciona nenhum valor adicional. É apenas mais código para ler e manter.
  • O teste falha de qualquer maneira quando service é null e vemos a verdadeira causa raiz da falha. O teste ainda cumpre seu propósito.
  • O erro produzido é ainda melhor com a afirmação AssertJ.

Veja a afirmação de teste modificada abaixo.

Java

 

@Test
public void getMessage() {
	assertThat(service.getMessage()).isEqualTo("Hello world!");
}

O teste modificado produz um erro como este:

Java

 

java.lang.NullPointerException: Cannot invoke "com.github.aha.poc.junit.spring.HelloService.getMessage()" because "this.service" is null
	at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)

Nota: O exemplo pode ser encontrado em SimpleSpringTest.

Afirme Valores e Não o Resultado

De vez em quando, escrevemos um teste correto, mas de uma maneira “ruim”. Significa que o teste funciona exatamente como pretendido e verifica nosso componente, mas a falha não está fornecendo informações suficientes. Portanto, nosso objetivo é afirmar o valor e não o resultado da comparação.

Má Prática

Vejamos alguns desses testes ruins:

Java

 

// #1
assertThat(argument.contains("o")).isTrue();

// #2
var result = "Welcome to JDK 10";
assertThat(result instanceof String).isTrue();

// #3
assertThat("".isBlank()).isTrue();

// #4
Optional<Method> testMethod = testInfo.getTestMethod();
assertThat(testMethod.isPresent()).isTrue();

Alguns erros dos testes acima são mostrados abaixo.

Plain Text

 

#1
Expecting value to be true but was false
	at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
	at com.github.aha.poc.junit5.params.SimpleParamTests.stringTest(SimpleParamTests.java:23)
  
#3
Expecting value to be true but was false
	at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
	at com.github.aha.poc.junit5.ConditionalTests.checkJdk11Feature(ConditionalTests.java:50)

Boa Prática

A solução é bastante fácil com AssertJ e sua API fluente. Todos os casos mencionados acima podem ser facilmente reescritos como:

Java

 

// #1
assertThat(argument).contains("o");

// #2
assertThat(result).isInstanceOf(String.class);

// #3
assertThat("").isBlank();

// #4
assertThat(testMethod).isPresent();

Os mesmos erros mencionados anteriormente agora fornecem mais valor.

Plain Text

 

#1
Expecting actual:
  "Hello"
to contain:
  "f" 
	at com.github.aha.poc.junit5.params.SimpleParamTests.stringTest(SimpleParamTests.java:23)
    
#3
Expecting blank but was: "a"
	at com.github.aha.poc.junit5.ConditionalTests.checkJdk11Feature(ConditionalTests.java:50)

Observação: O exemplo pode ser encontrado em SimpleParamTests.

Agrupar Afirmações Relacionadas

A encadeamento de afirmações e a indentação do código relacionado ajudam muito na clareza e legibilidade dos testes.

Prática Ruim

Ao escrevermos um teste, podemos acabar com um teste correto, mas menos legível. Vamos imaginar um teste onde queremos encontrar países e fazer essas verificações:

  1. Conte os países encontrados. 
  2. Afirme a primeira entrada com vários valores.

Tais testes podem se parecer com este exemplo:

Java

 

@Test
void listCountries() {
	List<Country> result = ...;

	assertThat(result).hasSize(5);
	var country = result.get(0);
	assertThat(country.getName()).isEqualTo("Spain");
	assertThat(country.getCities().stream().map(City::getName)).contains("Barcelona");
}

Prática Boa

Embora o teste anterior seja correto, devemos melhorar muito a legibilidade agrupando as afirmações relacionadas (linhas 9-11). O objetivo aqui é afirmar result uma vez e escrever muitas afirmações encadeadas conforme necessário. Veja a versão modificada abaixo.

Java

 

@Test
void listCountries() {
	List<Country> result = ...;

	assertThat(result)
		.hasSize(5)
		.singleElement()
		.satisfies(c -> {
			assertThat(c.getName()).isEqualTo("Spain");
			assertThat(c.getCities().stream().map(City::getName)).contains("Barcelona");
		});
}

Observação: O exemplo pode ser encontrado em CountryRepositoryOtherTests.

Evitar Testes de Sucesso Falsamente Positivos

Quando qualquer método de afirmação que utiliza o argumento ThrowingConsumer é empregado, então o argumento deve incluir assertThat no consumidor também. Caso contrário, o teste passaria sempre – mesmo quando a comparação falha, o que significa um teste incorreto. O teste só falha quando uma afirmação lança uma exceção RuntimeException ou AssertionError. Suponho que isso esteja claro, mas é fácil esquecer e escrever o teste errado. Isso acontece comigo de vez em quando.

Prática Ruim

Imaginemos que temos alguns códigos de países e queremos verificar se cada código satisfaz uma determinada condição. No nosso caso fictício, queremos afirmar que cada código de país contém o caractere “a”. Como você pode ver, isso é um absurdo: temos códigos em maiúsculas, mas não estamos aplicando insensibilidade à caixa na afirmação.

Java

 

@Test
void assertValues() throws Exception {
	var countryCodes = List.of("CZ", "AT", "CA");
	
	assertThat( countryCodes )
		.hasSize(3)
		.allSatisfy(countryCode -> countryCode.contains("a"));
}

Surpreendentemente, nosso teste passou com sucesso.

Prática Boa

Como mencionado no início desta seção, nosso teste pode ser facilmente corrigido com a adição de assertThat no consumidor (linha 7). O teste correto deve ser assim:

Java

 

@Test
void assertValues() throws Exception {
	var countryCodes = List.of("CZ", "AT", "CA");
	
	assertThat( countryCodes )
		.hasSize(3)
		.allSatisfy(countryCode -> assertThat( countryCode ).containsIgnoringCase("a"));
}

Agora o teste falha como esperado com a mensagem de erro correta.

Plain Text

 

java.lang.AssertionError: 
Expecting all elements of:
  ["CZ", "AT", "CA"]
to satisfy given requirements, but these elements did not:

"CZ"
error: 
Expecting actual:
  "CZ"
to contain:
  "a"
 (ignoring case)
	at com.github.aha.sat.core.clr.AppleTest.assertValues(AppleTest.java:45)

Encadeamento de Afirmações

A última dica não é propriamente uma prática, mas sim uma recomendação. A API fluente do AssertJ deve ser utilizada para criar testes mais legíveis.

Afirmações Não Encadeadas

Consideremos o teste listLogs, cujo objetivo é testar o registro de um componente. O objetivo aqui é verificar:

  • Número afirmado de logs coletados
  • Afirmar a existência de mensagens de log DEBUG e INFO
Java

 

@Test
void listLogs() throws Exception {
	ListAppender<ILoggingEvent> logAppender = ...;
	
	assertThat( logAppender.list ).hasSize(2);
	assertThat( logAppender.list ).anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(DEBUG);
			assertThat( logEntry.getFormattedMessage() ).startsWith("Initializing Apple");
		});
	assertThat( logAppender.list ).anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(INFO);
			assertThat( logEntry.getFormattedMessage() ).isEqualTo("Here's Apple runner" );
		});
}

Encadeamento de Afirmações

Com a API fluente mencionada e o encadeamento, podemos alterar o teste desta maneira:

Java

 

@Test
void listLogs() throws Exception {
	ListAppender<ILoggingEvent> logAppender = ...;
	
	assertThat( logAppender.list )
		.hasSize(2)
		.anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(DEBUG);
			assertThat( logEntry.getFormattedMessage() ).startsWith("Initializing Apple");
		})
		.anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(INFO);
			assertThat( logEntry.getFormattedMessage() ).isEqualTo("Here's Apple runner" );
		});
}

Nota: o exemplo pode ser encontrado em AppleTest.

Resumo e Código Fonte

A framework AssertJ oferece muito auxílio com sua API fluente. Neste artigo, foram apresentadas várias dicas e sugestões para produzir testes mais claros e confiáveis. Por favor, tenha em mente que a maioria dessas recomendações é subjetiva. Depende das preferências pessoais e do estilo de código.

O código fonte utilizado pode ser encontrado nos meus repositórios:

Source:
https://dzone.com/articles/hints-for-unit-testing-with-assertj