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á
- Como escrever testes unitários limpos e legíveis com JUnit e frameworks de assert
- Como evitar falsos positivos em alguns casos
- O que evitar ao escrever testes unitários
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:
@Test
public void getMessage() {
assertThat(service).isNotNull();
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
Este teste produz erros como este:
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.
@Test
public void getMessage() {
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
O teste modificado produz um erro assim:
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:
// #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.
#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:
// #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.
#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:
- Conte os países encontrados.
- Afirme a primeira entrada com vários valores.
Tal teste pode se parecer com este exemplo:
@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.
@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.
@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:
@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.
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
eINFO
@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:
@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