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á
- Como escrever testes de unidade limpos e legíveis com JUnit e frameworks de afirmação
- Como evitar testes falsos positivos em alguns casos
- O que evitar ao escrever testes de unidade
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:
@Test
public void getMessage() {
assertThat(service).isNotNull();
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
Este teste produz erros assim:
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.
@Test
public void getMessage() {
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
O teste modificado produz um erro como este:
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:
// #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)
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:
// #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)
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:
- Conte os países encontrados.
- Afirme a primeira entrada com vários valores.
Tais testes podem 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");
}
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.
@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.
@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)
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
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 alterar 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 fonte utilizado pode ser encontrado nos meus repositórios:
Source:
https://dzone.com/articles/hints-for-unit-testing-with-assertj