Java JUnit Parameterized Tests
Run the same JUnit test with different inputs using @ParameterizedTest and value sources.
Java JUnit Parameterized Tests
A parameterized test runs the same test method many times, once for each set of inputs you feed it. Instead of copy-pasting testReverseAbc, testReverseEmpty, and testReverseSingle, you write the logic once and supply a data source — a list of inputs and expected results. JUnit 5 (the Jupiter engine) makes this first-class with @ParameterizedTest and a family of source annotations. The payoff is fewer lines, denser coverage, and each input reported as its own pass/fail.
From repeated tests to one parameterized test
A plain @Test method tests exactly one scenario. When you want to check the same behaviour across a table of inputs, the naive approach repeats the method:
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)); }
}The parameterized version collapses all three into one method. You annotate with @ParameterizedTest (not @Test) and attach a source that supplies the argument for each run:
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));
}
}JUnit invokes isPrime three times — candidate=2, then 7, then 13 — and reports three results. One failing value does not hide the others.
Choosing an argument source
The @ParameterizedTest annotation is useless on its own; it needs a source that produces the arguments. JUnit Jupiter ships several, each suited to a different shape of data:
| Source | Supplies | Best for |
|---|---|---|
@ValueSource | A single literal per run (ints, strings, doubles, …) | One-argument tests |
@CsvSource | A row of comma-separated values per run | A few inline rows with multiple columns |
@CsvFileSource | Rows read from a classpath .csv file | Large or externally maintained tables |
@MethodSource | Whatever a factory method returns as a Stream/Collection | Complex objects, computed cases |
@EnumSource | The constants of an enum | Exhaustively covering an enum |
@NullSource / @EmptySource | null and empty values | Edge-case coverage of strings/collections |
The rule of thumb: @ValueSource for one simple input, @CsvSource for a small multi-column table, and @MethodSource once the data stops fitting in annotation literals.
Multiple columns with @CsvSource
When each case has an input and an expected output, @CsvSource gives you a tiny table inline. Each string is one row; commas split it into method parameters in order:
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));
}
}JUnit converts each comma-separated token to the declared parameter type, so @CsvSource({"4, 16"}) can land in (int n, int square). Use single quotes to include commas or empty strings inside a cell.
Computed cases with @MethodSource
Annotation values must be compile-time constants, so once arguments are real objects or need computing, switch to @MethodSource. It names a static method that returns a Stream<Arguments> (or any 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));
}
}The optional name attribute customises how each invocation appears in the test report, with {0}, {1} standing in for the arguments — invaluable when a single failing row needs to be identified at a glance.
A worked example: a parameterized runner without JUnit
The code runner has no JUnit on its classpath, so this program models the mechanism a parameterized test embodies with plain JDK code: a single check is defined once, then driven over a list of cases — exactly what @ParameterizedTest does behind the annotations. One case is wrong on purpose so you can see how isolated rows pass or fail.
What to take from the run:
- The
reverseblock prints fourPASSlines and>> reverse: 4 passed, 0 failed— one body (reverse) ran against four rows, mirroring how a single@ParameterizedTestmethod is invoked once per@CsvSourcerow. - The
isPrimeblock printsPASSfor inputs2,7,9, and1, butFAILfor input4, becauseisPrime(4)returnsfalsewhile the row claimedtrue— a wrong expectation, not a code bug, which is the most common parameterized-test mistake. - That single failure is reported on its own line and counted as
>> isPrime: 4 passed, 1 failed; the other rows still pass, demonstrating the key advantage over a hand-rolled loop with one assertion — each input is an independent, individually reported case. - The
runAllhelper takes the unit as aFunctionand the cases as aList, separating the logic under test from the data — exactly the separation@ParameterizedTestplus an argument source gives you. - Every line shows
expectednext toactual, so the4 / expected=true / actual=falserow tells you precisely which value disagreed — the same diagnostic value JUnit'sassertEqualsmessage and thename = "..."template provide.
Practice
In JUnit 5, your test needs to run once for each row of a table where every row has both an input string and the expected reversed string. Which is the most appropriate single annotation to supply that data inline?