Turbinando sua aplicação Java com Testes Parametrizados
A partir da versão 4 do Junit podemos usar testes parametrizados (Parameterized Tests) para reduzir, ou até mesmo remover completamente, redundância no nosso código de teste. Tá, mas o que exatamente é essa feature?
De acordo com Baeldung, essa feature “nos permite executar um único teste múltiplas vezes com diferentes parâmetros”.
Ao final desse artigo, teremos visto algumas formas de aplicar isso na sua rotina diária de testes (e ao dizer isso, presumo que você já está ciente da importância em testar seu código) com alguns exemplos que irei mostrar aqui.
(Também deixei um bônus no final do artigo 😄)
1 — Getting Started
Como pré-requisito, é importante ter conhecimentos básicos em Spring Boot, Java e Junit. Sim, sim, não precisa ser um sayajin pra começar a usar esse tipo de teste.
Isso é o que estou usando para esse tutorial:
- Spring Boot 2.5.3
- Maven
- JUnit 5
- Java 8+
1.1 — Adicionando a dependência do JUnit
Depois de preparar seu projeto (E pra isso recomendo usar o spring initializr website ), adicione o seguinte no pom.xml.
A primeira dependência não conta só com JUnit, também vem outras libs que vão te ajudar nos seus testes (mockito and hamcrest). Usaremos a segunda dependência quando formos ver os "arguments provider" (relaxa, está logo ali abaixo 😉)
2 — Colocando a mão na massa!
2.1 — Usando um único argumento
Vamos trabalhar com a função abaixo, que verifica se uma idade é maior que 21 anos e lança uma exceção se a idade for igual ou menor a 0.
Agora, vamos criar um teste para nos certificar que a exceção será lançada no momento correto, sob diferentes argumentos.
Devemos fazer uso das anotações @ParameterizedTest e @ValueSource para indicar ao JUnit que vamos passar diferentes argumentos. Essa anotação nos permite trabalhar com uma série de tipos de dados (shorts, bytes, ints, longs, floats, doubles, chars, booleans, strings e classes)
Fornecemos 4 argumentos, o que significa que o teste vai rodar 4 vezes. Portanto, na primeira iteração, a variável age assumirá o valor de 10, depois 0 e assim por diante.
Uma vez que os testes finalizarem, podemos ver o resultado de cada iteração.
2.2 — Usando múltiplos argumentos
Vamos criar uma função que multiplica dois integers.
public int calculateTotalPrice(int productPrice, int quantity) {
return productPrice * quantity;
}
Se passássemos como argumento os valores 2 e 2, naturalmente teríamos o resultado igual a 4. Sem mágica aqui.
E tudo bem, o teste que a gente fez não tem nenhum problema. Mas tem uma coisa que a gente precisa ter em mente: como bom desenvolvedor, você percebe que esse teste, apesar de funcionar e estar correto, não engloba todas as possibilidades existentes e que você precisa testar sua função com diferentes valores para se certificar que ela vai se comportar corretamente em qualquer cenário e sob qualquer circunstância (por exemplo, com valores negativos ou iguais a 0).
Uma forma de fazer isso seria criar um teste para cada um destes cenários. E o grande problema nisso seria a grande repetição de código de teste que você teria que dar manutenção no futuro. Para o método que criamos, com dois argumentos e (digamos) 4 cenários, não ficaria algo muito grande. Mas imagine um método mais complexo, com mais argumentos e mais cenários… ia ser o caos instaurado na terra.
E é nesse momento que os testes parametrizados brilham com força! Vamos dar uma olhada no código.
O que está acontecendo aqui é: Estamos usando a anotação @CsvSource juntamente com @ParameterizedTest.
Na primeira iteração estaremos usando os dados da primeira linha: “2, 3, 6”, que são "automagicamente" repassados como argumento à função por as ter declarado na assinatura do método do teste.
Vale mencionar: os argumentos são linkados na exata ordem que você as declarou. Sendo assim, na primeira iteração temos:
- productPrice = 2
- quantity = 3
- totalExpected = 6
Agora, eu acredito que você deve estar se questionando “como que isso funciona se o método calculatedTotalPrice exige int e eu passei String?”. E a resposta é que Junit cuida disso pra gente ❤.
Isso é chamado de "Argument Conversion". Apesar de ser um assunto que foge do escopo desse artigo, acho válido mencionar brevemente.
Só por curiosidade, segue um vislumbre do que "Arguments Conversion" pode fazer pra gente. Preste atenção nos valores passados em @ValueSource:
Sim, eu sei… chega a ser impressionante. Ele automaticamente converte a string em LocalDate. Se você deseja se aprofundar nesse tópico, eu sugiro fortemente checar essa documentação para verificar as dezenas de conversões implícitas que compõem essa funcionalidade.
2.3 — Usando um arquivo CSV
No exemplo anterior nós usamos a anotação @CsvSource. Esse método pode vir a ser um problema se você tiver um método com muitos argumentos e/ou muitos cenários, por tender a deixar seu código de teste visualmente bagunçado e até menos legível (imagine se ao invés de 3 integers, tivéssemos 5 strings como argumentos em 8 possíveis cenários diferentes…).
Mas não se preocupe, há uma forma de driblar este empecilho ao fazer uso de arquivos csv mesmo.
Primeiramente, vamos criar o arquivo testData.csv no diretório src/test/resources e adicionar o seguinte:
2, 3, 6
0, 10, 0
-5, 8, -40
3, -10, -30
-3, -13, 39
10, 15, 150
9, 9, 81
Dessa vez vamos usar a anotação @CsvFileSource, que possui um argumento chamado “resources”. Nele, podemos informar o path do csv base para o teste (se você colocou no diretório mencionado ali em cima, só precisa colocar "/testData.csv")
Dessa forma, independente da quantidade de argumentos e de cenários, você vai ter um código visualmente limpo e mais agradável ❤
2.4 — Enum Source
A anotação @EnumSource nos permite usar todos os elementos de um Enum em um teste. Dessa forma, você pode aplicar qualquer regra de negócio que for necessário.
2.5 — Method Source
Algumas vezes você pode ter a necessidade de usar os mesmos argumentos de parâmetros em mais de um teste. E isso pode ser uma boa hora para usar a anotação @MethodSource.
Essa forma consiste em usar uma função externa como fornecedora de argumentos (arguments provider). Assim, você consegue compartilhar seus argumentos com vários testes.
Como ponto de atenção, note que a função dataProvider pode ser escrita em qualquer diretório que você deseja, desde que você forneça o path completo na anotação. O nome do método deve vir após a "#".
3 — Bonus ❤
Como prometido, um bônus pra você ❤
É possível customizar a forma como o resultado dos testes é mostrado após sua execução. E para que fique claro, deixe-me mostrar o que quero dizer.
Vamos relembrar esse teste:
Após rodá-lo, eis o que aparece no display (usando Intellij):
Você pode fazer uso de placeholders e melhorar a forma como essa informação aparece:
- {index} — representa o número da iteração atual (começa no 1)
- {0} , {1} … — representa o nome da variável. É usado quando se tem mais de 1 parâmetro.
Dito isso, a única alteração que é necessário fazer é incluir o argumento name dentro da anotação @ParameterizedTest com o padrão que você deseja.
Dessa forma, temos no console o que se vê abaixo:
Muito obrigado por ter lido este artigo. Espero que tenha aproveitado e aprendido algo hoje! ❤