Java JUnit Parameterized Tests
Run the same JUnit test with different inputs using @ParameterizedTest and value sources.
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.
This chapter assumes you already know how a plain test is written and asserted; if not, start with the JUnit introduction and JUnit assertions. It covers when to reach for a parameterized test, how to pick an argument source (@ValueSource, @CsvSource, @MethodSource, and others), and the most common mistake — a wrong expected value rather than a code bug.
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.
When to use a parameterized test
Reach for @ParameterizedTest when one behaviour should hold across a table of inputs — boundary values, equivalence classes, or a regression list of inputs that once broke. Keep using a plain @Test when a scenario needs unique setup or distinct assertions; cramming unrelated cases into one parameterized method only makes the report harder to read. For shared setup across either style, see the test lifecycle chapter, and for the full assertion vocabulary used inside each run, the assertions chapter.