Dicas para Testes Unitários com AssertJ

Teste unitário tornou-se uma parte padrão do desenvolvimento. Muitas ferramentas podem ser utilizadas de diversas maneiras. Este artigo demonstra algumas dicas ou, digamos, melhores práticas que funcionaram 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 nosso principal interesse não é evitar NPE nos testes. Nosso objetivo é verificar o comportamento do componente testado de forma 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 como este:

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.
  • A mensagem de erro produzida é 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 assim:

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.

Prática Ruim

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)

Prática Boa

A solução é bastante fácil com AssertJ e sua API fluente. Todas as situações mencionadas acima podem ser facilmente reescritas 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)

Nota: O exemplo pode ser encontrado em SimpleParamTests.

Agrupar Afirmações Relacionadas

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

Má Prática

À medida que escrevemos um teste, podemos acabar com um teste correto, mas menos legível. Imaginemos 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.

Tal teste pode 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");
}

Boa Prática

Embora o teste anterior esteja 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");
		});
}

Nota: O exemplo pode ser encontrado em CountryRepositoryOtherTests.

Evitar Teste de Sucesso Falso Positivo

Quando qualquer método de afirmação que utiliza o argumento ThrowingConsumer é empregado, então o argumento deve conter 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. Acho que está claro, mas é fácil esquecer e escrever o teste errado. Isso acontece comigo de vez em quando.

Prática Ruim

Imaginemos que temos um conjunto de códigos de países e queremos verificar se cada código satisfaz alguma 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, é um absurdo: temos códigos em maiúsculas, mas não estamos aplicando a 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)

Afirmações em Cadeia

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

Vamos considerar o teste listLogs, cujo propósito é testar o registro de um componente. O objetivo aqui é verificar:

  • Número de logs coletados afirmado
  • Afirma 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 mudar 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 utilizado pode ser encontrado em minhas repositórias:

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