Dominando Testes Parametrizados no JUnit 6
Depois de concluirmos esta aula, nós:
- Saberemos como adicionar as dependências necessárias do JUnit 6 para testes parametrizados com Gradle e Maven.
- Saberemos como escrever testes parametrizados simples usando a anotação
@ParameterizedTest. - Saberemos como customizar o nome de exibição (display name) de cada invocação do nosso teste parametrizado.
- Compreenderemos como utilizar todas as diferentes fontes de argumentos (argument sources) fornecidas pelo JUnit Jupiter:
@ValueSource,@EnumSource,@CsvSource,@CsvFileSource,@MethodSourcee@ArgumentsSource.
Vamos começar obtendo as dependências necessárias.
1. Obtendo as Dependências Necessárias
Para escrever testes parametrizados com JUnit 6 (JUnit Jupiter 6.0.3), precisamos declarar a dependência junit-jupiter-params no nosso build:
- Name: JUnit Jupiter Params
- Group ID:
org.junit.jupiter - Artifact ID:
junit-jupiter-params - Version:
6.0.3(gerenciada automaticamente pelo BOMjunit-bomdo Spring Boot 4.1.2)
Com Gradle 9.8.2 (Kotlin DSL)
Se estivermos usando o plugin jvm-test-suite ou o spring-boot-starter-test, o artefato junit-jupiter-params já vem incluído transitivamente por meio do artefato agregador org.junit.jupiter:junit-jupiter. Caso você configure as dependências do JUnit manualmente no build.gradle.kts, certifique-se de adicioná-lo no escopo testImplementation:
|
|
Com Maven
Se você utiliza Maven com junit-bom, adicione a dependência junit-jupiter-params com escopo test:
|
|
A seguir, vamos escrever nosso primeiro teste parametrizado com JUnit 6.
2. Escrevendo Nosso Primeiro Teste Parametrizado
Para criar um teste parametrizado simples com JUnit Jupiter, precisamos seguir dois passos:
- Anotar o método de teste com
@ParameterizedTest(em vez de@Test). - Configurar a fonte de dados que fornece os parâmetros do método usando uma anotação de argument source. Neste primeiro exemplo, passaremos duas
Strings ("Hello"e"World") utilizando a anotação@ValueSource:
|
|
Quando executamos este teste na IDE ou no terminal, vemos que o JUnit 6 invoca nosso método de teste duas vezes — uma para cada elemento do array strings — e exibe cada invocação individualmente no relatório:
|
|
Como podemos notar na saída acima, o nome de exibição padrão de cada invocação contém o índice da invocação ([1], [2]) e a representação String de todos os parâmetros passados ao método. Contudo, podemos customizar esse formato facilmente.
3. Customizando o Nome de Exibição de Cada Invocação
Podemos customizar o nome de exibição (display name) de cada invocação configurando o atributo name da anotação @ParameterizedTest. Dentro dessa String de formatação, podemos utilizar os seguintes placeholders:
{index}: é substituído pelo índice da invocação atual. O índice da primeira invocação é1, o da segunda é2, e assim por diante.{arguments}: é substituído pela lista completa de argumentos separados por vírgula passados ao método de teste.{0},{1}, …: é substituído pelo valor de um argumento individual na posição correspondente (onde{0}é o primeiro argumento,{1}é o segundo, etc.).
[!IMPORTANT] Atenção às aspas simples no
MessageFormatInternamente, o JUnit Jupiter utiliza a classe
java.text.MessageFormatpara formatar o nome de exibição da invocação. NoMessageFormat, uma aspa simples (') é tratada como caractere de escape! Portanto, se você quiser exibir uma aspa simples literal no nome do seu teste, precisará escrever duas aspas simples consecutivas ('').
Vamos atualizar nosso teste anterior para exibir o formato "{index} => message=''{0}''":
|
|
Ao executar novamente o teste, o relatório mostrará nossas invocações com o template customizado:
|
|
Agora que sabemos como criar e formatar um teste parametrizado, vamos explorar todas as fontes de argumentos disponíveis no JUnit 6.
4. Especificando as Fontes de Argumentos (Argument Sources)
O módulo junit-jupiter-params oferece diversas anotações para fornecer dados aos nossos testes parametrizados. Vamos analisar cada uma delas em detalhes.
4.1. @ValueSource
A anotação @ValueSource é a fonte de argumentos mais simples: ela permite configurar um único array de valores literais e só pode ser utilizada em testes parametrizados que recebem exatamente um argumento por invocação.
Ela suporta arrays de:
shorts(ShortArray)bytes(ByteArray)ints(IntArray)longs(LongArray)floats(FloatArray)doubles(DoubleArray)chars(CharArray)booleans(BooleanArray)strings(Array<String>)classes(Array<KClass<*>>)
[!NOTE] Complementarmente à
@ValueSource, o JUnit Jupiter também fornece as anotações@NullSource,@EmptySourcee@NullAndEmptySourcepara injetar valoresnulle strings/coleções vazias sem esforço adicional.
4.2. @EnumSource
A anotação @EnumSource permite passar valores de um enum para um teste parametrizado que recebe um único objeto Enum como argumento.
Considere o seguinte enum Pet:
|
|
Se quisermos executar nosso método de teste passando todos os valores disponíveis no enum Pet, basta anotar o método com @EnumSource(Pet::class) (no JUnit 6 em Kotlin, você pode inclusive omitir Pet::class se o tipo do parâmetro do método já for Pet, pois o JUnit infere o tipo automaticamente!):
|
|
Se quisermos restringir a execução do nosso teste para apenas valores específicos do enum Pet, podemos configurar o atributo names da anotação @EnumSource:
|
|
4.3. @CsvSource
Quando precisamos passar múltiplos argumentos para o nosso método de teste (como entradas e o resultado esperado de um cálculo), @ValueSource e @EnumSource não são suficientes. Nesses cenários, podemos usar @CsvSource.
Com @CsvSource, cada String do array representa uma linha CSV completa (uma invocação do método de teste), onde os valores separados por vírgula são convertidos automaticamente pelo JUnit para os tipos dos parâmetros do método (Int, Long, String, Boolean, LocalDate, etc.).
Vamos escrever um teste parametrizado que verifica se o método sum() calcula corretamente a soma de dois números inteiros:
|
|
[!TIP] Se você precisar passar uma
Stringque contenha vírgulas dentro do@CsvSource, basta envolvê-la em aspas simples, por exemplo:"1, 'Tarefa, urgente', true". Você também pode usardelimiter = ';'caso prefira separar colunas por ponto e vírgula.
4.4. @CsvFileSource
Se a tabela de casos de teste for extensa, embutir dezenas de linhas dentro do código-fonte com @CsvSource prejudica a leitura. Para resolver isso, podemos armazenar os dados em um arquivo .csv no classpath (por exemplo, src/test/resources/test-data.csv) e carregá-lo com @CsvFileSource.
Suponha que o arquivo src/test/resources/test-data.csv tenha o seguinte conteúdo:
|
|
Podemos configurar nosso teste parametrizado apontando o atributo resources da anotação @CsvFileSource para /test-data.csv:
|
|
[!NOTE] Se o seu arquivo CSV possuir uma linha de cabeçalho na primeira linha (ex:
a, b, sum), você pode instruir o JUnit a ignorá-la adicionandonumLinesToSkip = 1na anotação@CsvFileSource(resources = ["/test-data.csv"], numLinesToSkip = 1).
4.5. @MethodSource
As anotações anteriores funcionam muito bem para tipos primitivos, Strings e Enums, mas e quando precisamos passar objetos complexos de domínio, listas ou instâncias construídas dinamicamente? Para isso, utilizamos @MethodSource.
Com @MethodSource, especificamos o nome de um método factory que retorna um Stream, Iterable, Iterator ou array de instâncias de Arguments.
Regras importantes do método factory:
- Ele não deve aceitar argumentos.
- Cada elemento retornado deve ser uma instância de
Arguments— criada convenientemente pelo método estáticoArguments.of(a, b, c)— que contenha exatamente os objetos esperados pelos parâmetros do método de teste na mesma ordem. - No JUnit, o método factory precisa ser estático (
@JvmStaticdentro de umcompanion objectem Kotlin), a menos que a classe de teste esteja anotada com@TestInstance(TestInstance.Lifecycle.PER_CLASS).
Vamos ver como implementar MethodSourceExampleTest em Kotlin usando companion object com @JvmStatic:
|
|
[!TIP] Se você não informar o nome do método na anotação
@MethodSource(usando apenas@MethodSource()), o JUnit Jupiter procurará automaticamente por um método factory que tenha exatamente o mesmo nome do método de teste!
4.6. @ArgumentsSource
Se quisermos reutilizar a mesma lógica fornecedora de dados de teste em múltiplas classes de teste diferentes, podemos encapsulá-la em uma classe dedicada implementando a interface ArgumentsProvider e registrá-la com @ArgumentsSource.
Vamos fazer isso em dois passos:
Passo 1: Criar a classe CustomArgumentProvider que implementa ArgumentsProvider:
|
|
Passo 2: Anotar nosso método de teste com @ArgumentsSource(CustomArgumentProvider::class):
|
|
Resumo
Nesta aula bônus, dominamos os testes parametrizados no JUnit 6:
- O artefato
org.junit.jupiter:junit-jupiter-paramsfornece todo o suporte à parametrização no JUnit Jupiter. - Usamos
@ParameterizedTestcombinada com uma anotação de fonte de dados e podemos customizar o nome de cada execução usando{index},{arguments}e{0},{1}, etc. (lembrando de escapar aspas simples com''). - Para um único parâmetro simples, usamos
@ValueSourceou@EnumSource. - Para múltiplos parâmetros tabulares, usamos
@CsvSource(inline) ou@CsvFileSource(arquivo externo no classpath). - Para objetos complexos ou geração programática de dados, usamos
@MethodSource(na própria classe) ou@ArgumentsSourcecomArgumentsProvider(reutilizável entre várias classes).