Testes Parametrizados JUnit em Java
Execute o mesmo teste JUnit com diferentes entradas usando @ParameterizedTest e fontes de valores.
Um teste parametrizado executa o mesmo método de teste várias vezes, uma vez para cada conjunto de entradas fornecido. Em vez de copiar e colar testReverseAbc, testReverseEmpty e testReverseSingle, você escreve a lógica uma vez e fornece uma fonte de dados — uma lista de entradas e resultados esperados. O JUnit 5 (motor Jupiter) torna isso de primeira classe com @ParameterizedTest e uma família de anotações de fonte. O benefício são menos linhas, maior cobertura e cada entrada reportada como seu próprio aprovado/reprovado.
Este capítulo assume que você já sabe como escrever e verificar um teste simples; caso contrário, comece pela introdução ao JUnit e pelas asserções JUnit. Ele aborda quando usar um teste parametrizado, como escolher uma fonte de argumentos (@ValueSource, @CsvSource, @MethodSource e outras), e o erro mais comum — um valor esperado errado em vez de um bug no código.
De testes repetidos a um único teste parametrizado
Um método @Test simples testa exatamente um cenário. Quando você quer verificar o mesmo comportamento em uma tabela de entradas, a abordagem ingênua repete o método:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PrimesTest {
@Test void two_isPrime() { assertTrue(Primes.isPrime(2)); }
@Test void seven_isPrime() { assertTrue(Primes.isPrime(7)); }
@Test void thirteen_isPrime() { assertTrue(Primes.isPrime(13)); }
}A versão parametrizada condensa os três em um único método. Você anota com @ParameterizedTest (não @Test) e anexa uma fonte que fornece o argumento para cada execução:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PrimesTest {
@ParameterizedTest
@ValueSource(ints = {2, 7, 13})
void isPrime(int candidate) {
assertTrue(Primes.isPrime(candidate));
}
}O JUnit invoca isPrime três vezes — candidate=2, depois 7, depois 13 — e reporta três resultados. Um valor que falha não oculta os demais.
Escolhendo uma fonte de argumentos
A anotação @ParameterizedTest é inútil sozinha; ela precisa de uma fonte que produza os argumentos. O JUnit Jupiter oferece várias, cada uma adequada a um formato diferente de dados:
| Fonte | Fornece | Melhor para |
|---|---|---|
@ValueSource | Um único literal por execução (ints, strings, doubles, …) | Testes com um argumento |
@CsvSource | Uma linha de valores separados por vírgula por execução | Algumas linhas inline com múltiplas colunas |
@CsvFileSource | Linhas lidas de um arquivo .csv no classpath | Tabelas grandes ou mantidas externamente |
@MethodSource | O que um método fábrica retornar como Stream/Collection | Objetos complexos, casos computados |
@EnumSource | As constantes de um enum | Cobertura exaustiva de um enum |
@NullSource / @EmptySource | Valores null e vazios | Cobertura de casos extremos em strings/coleções |
A regra geral: @ValueSource para uma entrada simples, @CsvSource para uma pequena tabela com múltiplas colunas, e @MethodSource quando os dados deixam de caber em literais de anotação.
Múltiplas colunas com @CsvSource
Quando cada caso tem uma entrada e uma saída esperada, @CsvSource oferece uma pequena tabela inline. Cada string é uma linha; vírgulas a dividem nos parâmetros do método em ordem:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
class StringsTest {
@ParameterizedTest
@CsvSource({
"abc, cba",
"racecar, racecar",
"'', ''" // single quotes denote an empty string
})
void reverse(String input, String expected) {
assertEquals(expected, Strings.reverse(input));
}
}O JUnit converte cada token separado por vírgula para o tipo de parâmetro declarado, então @CsvSource({"4, 16"}) pode cair em (int n, int square). Use aspas simples para incluir vírgulas ou strings vazias dentro de uma célula.
Casos computados com @MethodSource
Valores de anotação devem ser constantes em tempo de compilação, então quando os argumentos são objetos reais ou precisam de computação, mude para @MethodSource. Ele nomeia um método estático que retorna um Stream<Arguments> (ou qualquer Collection/array):
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TaxTest {
static Stream<Arguments> brackets() {
return Stream.of(
Arguments.of(0, 0.0),
Arguments.of(10_000, 1_000.0),
Arguments.of(50_000, 7_500.0)
);
}
@ParameterizedTest(name = "income {0} -> tax {1}")
@MethodSource("brackets")
void computesTax(int income, double expectedTax) {
assertEquals(expectedTax, Tax.of(income));
}
}O atributo opcional name personaliza como cada invocação aparece no relatório de testes, com {0}, {1} representando os argumentos — indispensável quando uma única linha com falha precisa ser identificada rapidamente.
Um exemplo prático: um executor parametrizado sem JUnit
O executor de código não tem JUnit no classpath, portanto este programa modela o mecanismo que um teste parametrizado incorpora com código JDK simples: uma única verificação é definida uma vez e então executada sobre uma lista de casos — exatamente o que @ParameterizedTest faz por trás das anotações. Um caso está errado de propósito para que você possa ver como linhas isoladas passam ou falham.
O que extrair da execução:
- O bloco
reverseimprime quatro linhasPASSe>> reverse: 4 passed, 0 failed— um único corpo (reverse) executado contra quatro linhas, espelhando como um único método@ParameterizedTesté invocado uma vez por linha de@CsvSource. - O bloco
isPrimeimprimePASSpara as entradas2,7,9e1, masFAILpara a entrada4, porqueisPrime(4)retornafalseenquanto a linha afirmavatrue— uma expectativa errada, não um bug no código, que é o erro mais comum em testes parametrizados. - Essa falha única é reportada em sua própria linha e contada como
>> isPrime: 4 passed, 1 failed; as demais linhas ainda passam, demonstrando a principal vantagem sobre um loop manual com uma única asserção — cada entrada é um caso independente, reportado individualmente. - O auxiliar
runAllrecebe a unidade comoFunctione os casos comoList, separando a lógica sob teste dos dados — exatamente a separação que@ParameterizedTestmais uma fonte de argumentos oferece. - Cada linha mostra
expectedao lado deactual, então a linha4 / expected=true / actual=falseindica exatamente qual valor discordou — o mesmo valor diagnóstico que a mensagem deassertEqualsdo JUnit e o modeloname = "..."fornecem.
Quando usar um teste parametrizado
Use @ParameterizedTest quando um comportamento deve se manter em uma tabela de entradas — valores de fronteira, classes de equivalência ou uma lista de regressão de entradas que já causaram problemas. Continue usando @Test simples quando um cenário precisa de configuração única ou asserções distintas; forçar casos não relacionados em um único método parametrizado só torna o relatório mais difícil de ler. Para configuração compartilhada em ambos os estilos, veja o capítulo sobre ciclo de vida de testes, e para o vocabulário completo de asserções usado dentro de cada execução, o capítulo sobre asserções.