Using JUnit 5 Parameterized Tests to Reduce Duplication
Learn how to replace repetitive @Test methods with a single @ParameterizedTest that runs for each input set, plus tips on sources, converters, and pitfalls.
17 Mar 2026, 10:00 UTC

When you find yourself writing several @Test methods that differ only in the input values and the assertions are identical, you are duplicating boilerplate. JUnit 5’s @ParameterizedTest lets you declare the test logic once and have JUnit invoke it for each argument set supplied by an argument source. Each invocation appears as a separate test case in IDEs and reports, so you keep the benefits of isolated reporting while cutting down on repeated code.
Why use parameterized tests
The primary advantage is maintainability: change the test method body once and the change applies to every data point. You also get consistent assertions, which reduces the chance of forgetting to update one of many similar tests. Parameterized tests work with the same lifecycle callbacks (@BeforeEach, @AfterEach) that run around each invocation, while @BeforeAll and @AfterAll still run once per test class.
Worked example: testing a date parser
Suppose you have a utility method LocalDate parse(String text) that expects ISO‑8601 dates. You want to verify that valid strings parse correctly and that invalid strings throw DateTimeParseException. Instead of writing a separate @Test for each case, you can use a CSV source to feed both valid and invalid inputs.
Test class with @CsvSource and custom converter
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.ParameterizedTest;
import org.junit.jupiter.api.converter.ConvertWith;
import org.junit.jupiter.api.converter.SimpleArgumentConverter;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import static org.junit.jupiter.api.Assertions.*;
class DateParserTest {
private DateParser parser;
@BeforeEach
void setUp() {
parser = new DateParser(); // class under test
}
@ParameterizedTest(name = "[{index}] parse('{0}') → {1}")
@CsvSource({
"2023-01-01, 2023-01-01",
"2022-12-31, 2022-12-31",
"invalid, null",
"2023-13-01, null"
})
void parse(@ConvertWith(IsoDateConverter.class) String input, LocalDate expected) {
LocalDate result = parser.parse(input);
if (expected == null) {
assertThrows(DateTimeParseException.class, () -> parser.parse(input));
} else {
assertEquals(expected, result);
}
}
static class IsoDateConverter extends SimpleArgumentConverter {
@Override
protected Object convert(Object source, Class targetType) {
if (source instanceof String && targetType == LocalDate.class) {
String s = (String) source;
try {
return LocalDate.parse(s); // throws if invalid
} catch (DateTimeParseException e) {
return null; // signal invalid input
}
}
throw new IllegalArgumentException("Cannot convert " + source);
}
}
}
Explanation:
@ParameterizedTestmarks the method as a parameterized test.- The
nameattribute uses placeholders:{index}is the invocation number,{0}refers to the first argument (the raw CSV string), and{1}would be the second if we had more. @CsvSourceprovides comma‑separated rows; each row becomes one invocation.@ConvertWith(IsoDateConverter.class)tells JUnit to run ourSimpleArgumentConverterbefore the test method receives the argument. The converter attempts to parse the string into aLocalDate; on failure it returnsnull, which the test interprets as an expected exception.@BeforeEachcreates a freshDateParserfor every invocation, ensuring isolation.
Lifecycle callbacks
If you added @BeforeEach and @AfterEach methods, they would execute before and after each CSV row. @BeforeAll and @AfterAll would run only once for the whole class, useful for expensive setup like starting an embedded database.
Limits and common mistakes
While powerful, parameterized tests have a few pitfalls to watch for.
Cartesian explosion
If you combine multiple argument sources (e.g., two @MethodSource methods) on the same test method, JUnit does not support it; you must pick one source per method. Attempting to add several sources leads to a compilation error. If you need a combination, create a custom ArgumentsProvider that returns the Cartesian product yourself, but be aware that large products can generate thousands of invocations, slowing down your build and making reports hard to read.
Duplicate display names
The name attribute must produce a unique string for each invocation. If you omit {index} or rely solely on values that may repeat (e.g., two CSV rows with the same first column), IDEs and test reporters may show duplicate names, making it difficult to identify which invocation failed. Always include {index} or ensure the argument representation is unique.
Mixing sources on one method
As noted, you cannot annotate a single @ParameterizedTest with both @ValueSource and @CsvSource. If you need different kinds of data for the same test logic, either write separate parameterized test methods or implement a custom ArgumentsProvider that returns a stream of Arguments objects built from whatever sources you like.
Exceptions in providers or converters
If your ArgumentsProvider or ArgumentConverter throws an exception, JUnit treats it as a test error, not a failure. This distinction matters for CI pipelines that categorize errors differently (e.g., treating errors as unstable builds). Log or handle exceptions inside the provider/converter and, if appropriate, return an empty stream or a sentinel value that the test method can assert against.
How to verify
To confirm that the test behaves as expected:
- Run the test suite with your build tool, for example:
./gradlew test # or mvn test
Check the output: you should see four test executions corresponding to the CSV rows. The display names will follow the pattern you set, e.g., [0] parse('2023-01-01') → 2023-01-01.
System.out.println or logger statement inside IsoDateConverter.convert and observe that it prints for every row before any assertion output.If you change the CSV source to include a new row and re‑run the build, the test count should increase by one, demonstrating that the test method itself did not need modification.
By using parameterized tests you eliminate duplicate test methods, keep assertions centralized, and still retain granular reporting. Pay attention to source uniqueness, avoid Cartesian blow‑up, and handle conversion errors explicitly to keep your test suite reliable and fast.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.