Escrevendo Testes Parametrizados com JUnit 6
Ao concluirmos esta aula, nós:
- Compreenderemos o que é um teste parametrizado e quando utilizá-lo.
- Entenderemos como o antigo runner
Parameterizeddo JUnit 4 funcionava e por que não devemos utilizar abordagens baseadas em injeção via construtor da classe de teste. - Seremos capazes de escrever testes parametrizados limpos e expressivos utilizando
@ParameterizedTeste@CsvSourcedo módulojunit-jupiter-paramsno JUnit 6 (que incorporou nativamente a filosofia da bibliotecaJUnitParams).
[!NOTE] Esta aula assume que:
- Você está familiarizado com classes de teste no JUnit 6.
- Você conhece a arquitetura do JUnit Platform.
- Você sabe como executar testes unitários utilizando Maven ou Gradle.
O Que É um Teste Parametrizado?
Um teste parametrizado (parameterized test) é um caso de teste que é invocado múltiplas vezes utilizando diferentes conjuntos predefinidos de entrada (input) e saída esperada (expected output). Esses testes são tipicamente utilizados para testar classes ou funções puras de domínio que não possuem dependências externas e cujos métodos não possuem efeitos colaterais (como cálculos, validações, regras de preço, máquinas de estado ou conversores).
Vamos conhecer o Sistema Sob Teste que utilizaremos nesta aula.
Introdução ao Sistema Sob Teste (SUT)
Durante esta aula, escreveremos testes unitários para uma classe simples chamada Calculator. Ela possui um método chamado sum(), que retorna a soma de dois números inteiros (Int). O código-fonte da classe Calculator em Kotlin é o seguinte:
|
|
A seguir, vamos analisar primeiro como os testes parametrizados eram escritos no JUnit 4 padrão (Parameterized), entender seus problemas estruturais e, em seguida, ver como a biblioteca JUnitParams inspirou o módulo oficial junit-jupiter-params do JUnit 6!
Contexto Histórico: O Problema do Runner Parameterized do JUnit 4
No JUnit 4 padrão, escrever um teste parametrizado exigia cinco passos burocráticos:
- Anotar a classe de teste inteira com
@RunWith(Parameterized::class). - Criar um método estático anotado com
@Parametersretornando umaCollection<Array<Any>>com os dados de entrada e saída esperada ({ 0, 0, 0 }, { 1, 1, 2 }). - Declarar campos finais na classe de teste para armazenar o
Calculator,first,secondeexpectedSum. - Criar um construtor na classe de teste que recebia
first,secondeexpectedSumpara inicializar os campos. - Escrever o método
@Testlendo os campos da classe.
Veja a quantidade de código boilerplate que o runner Parameterized do JUnit 4 exigia para testar uma simples soma:
|
|
Embora essa abordagem funcionasse, ela tinha dois defeitos graves:
- Exigia muito código boilerplate: declarar método estático de arrays de
Object, campos de classe e construtor tornava o teste difícil de escrever e ler. - Amarrava a classe inteira a um único conjunto de parâmetros: Como os dados de teste eram passados pelo construtor da classe de teste, era impossível ter dois métodos de teste na mesma classe que recebessem parâmetros diferentes (por exemplo, um método para testar
sum()e outro para testarmultiply()ousqrt())!
No JUnit 4, a comunidade resolvia esse problema adicionando a biblioteca externa pl.pragmatists:JUnitParams (@RunWith(JUnitParamsRunner.class) + @Parameters({ "0, 0, 0", "1, 1, 2" })), que permitia passar os parâmetros diretamente nos argumentos do método de teste!
Quando o JUnit 5 e o JUnit Jupiter 6 foram projetados, a equipe do JUnit absorveu a exata ideia do JUnitParams para dentro do pacote oficial org.junit.jupiter:junit-jupiter-params! Vamos ver como utilizá-lo agora.
Escrevendo Testes Parametrizados com junit-jupiter-params (e o Paralelo com JUnitParams)
1. Obtendo as Dependências Necessárias
Para escrever testes parametrizados no JUnit Jupiter 6, declaramos a dependência oficial junit-jupiter-params no nosso script de build (nota: se você já usa o agregador org.junit.jupiter:junit-jupiter ou spring-boot-starter-test, o junit-jupiter-params já está incluído automaticamente!).
Se estivermos declarando as dependências individualmente no Maven, adicionamos ao pom.xml:
|
|
Se estivermos utilizando Gradle 9 (Kotlin DSL), adicionamos ao build.gradle.kts:
|
|
(Para comparação histórica: no JUnit 4 usava-se pl.pragmatists:JUnitParams:1.0.5).
Vamos agora reescrever nosso teste da Calculator de forma limpa passo a passo!
2. Escrevendo o Teste Unitário Parametrizado Passo a Passo
Podemos garantir que o método sum() da classe Calculator funciona como esperado seguindo estes passos:
Passo 1: Criar a classe de teste ParameterizedCalculatorTest
Primeiro, criamos nossa classe de teste. No JUnit 6, não precisamos de @RunWith(JUnitParamsRunner::class) — o motor Jupiter já suporta métodos parametrizados nativamente (inclusive dentro de classes @Nested!):
|
|
Passo 2: Escrever o método de teste recebendo os parâmetros diretamente
Segundo, adicionamos o método de teste à nossa classe de teste seguindo estes passos:
- Declaramos o método
shouldReturnCorrectSumrecebendo a entrada fornecida (first: Int,second: Int) e a saída esperada (expectedSum: Int) diretamente como parâmetros do método. - Instanciamos
Calculator, invocamoscalculator.sum(first, second)e verificamos com o AssertJ se a soma retornada é igual aexpectedSum.
Após escrevermos o método, o código fica assim:
|
|
Passo 3: Configurar os parâmetros com @ParameterizedTest e @CsvSource
Terceiro, configuramos os parâmetros que serão passados para o nosso método de teste seguindo estes dois passos:
- Anotamos o método de teste com
@ParameterizedTest(em vez de@Test). - Anotamos o método com
@CsvSource(o equivalente direto ao@Parameters({ "0, 0, 0", "1, 1, 2" })doJUnitParams), especificando as linhas de entrada e saída esperada separadas por vírgula.
Após configurarmos os parâmetros do nosso teste parametrizado, o código-fonte completo da nossa classe de teste fica assim:
|
|
Como podemos ver, @ParameterizedTest + @CsvSource (assim como o JUnitParams fazia no JUnit 4) resolve todos os problemas do antigo runner Parameterized:
- Zero código boilerplate: não há construtor na classe de teste, nem propriedades intermediárias.
- Parâmetros injetados diretamente no método: podemos ter múltiplos métodos
@ParameterizedTestna mesma classe usando conjuntos de parâmetros completamente diferentes, e podemos misturar métodos@Testnormais e classes@Nestedna mesma classe de teste! - Fontes de dados ricas: além de
@CsvSource, ojunit-jupiter-paramssuporta nativamente leitura de arquivos CSV externos (@CsvFileSource), enums (@EnumSource), valores simples (@ValueSource), métodos provedores (@MethodSource) e classes provedoras customizadas (@ArgumentsSource) — que exploraremos ainda mais nas aulas avançadas!
Resumo
Nesta aula, aprendemos quatro pontos essenciais:
- Não devemos usar o modelo antigo de testes parametrizados via construtor da classe de teste (
Parameterizeddo JUnit 4), pois ele exige código boilerplate excessivo e impede múltiplos testes com assinaturas diferentes na mesma classe. - Para escrever testes parametrizados modernos no JUnit 6, utilizamos o módulo oficial
org.junit.jupiter:junit-jupiter-params(que sucedeu a bibliotecaJUnitParams). - Anotamos o método de teste com
@ParameterizedTeste@CsvSourcepara injetar cada linha de dados de entrada e saída esperada diretamente nos parâmetros do método de teste. - Também podemos fornecer dados a partir de provedores customizados (
@MethodSource,@ArgumentsSource) e fontes externas como arquivos CSV (@CsvFileSource).