Using JUnit 5 @ParameterizedTest to Eliminate Duplicate Test Code
Learn how JUnit 5's @ParameterizedTest lets you write one test method and run it with multiple argument sets, reducing boilerplate and improving test clarity.
05 Sept 2025, 18:46 UTC

The problem: repetitive test methods
When you need to verify the same behavior with different inputs, it's tempting to copy-paste a test method and change only the argument values. This leads to a test class that grows linearly with the number of data points, making it harder to maintain and obscuring the intent of the test.
Thesis: @ParameterizedTest lets you write the test logic once and feed it multiple argument sets
JUnit 5's @ParameterizedTest annotation runs the same test method for each element supplied by an argument source such as @ValueSource, @CsvSource, or a custom @MethodSource. The feature reduces boilerplate, improves readability, and integrates with JUnit 5's extension model for better reporting.
How @ParameterizedTest works
- The test method must accept parameters that match the type and order of the values supplied by the source.
- Each invocation gets a fresh test instance by default (
@TestInstance(Lifecycle.PER_METHOD)), ensuring isolation between invocations. - You can customize how each invocation appears in reports using
@DisplayNameGenerationor a customDisplayNameGenerator.
Worked example: validating case-insensitive string equality
Suppose you want to verify that String.equalsIgnoreCase returns true for several pairs of strings.
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;
@DisplayNameGeneration(ReplaceUnderscores.class)
class StringEqualsIgnoreCaseTest {
@ParameterizedTest
@ValueSource(strings = {"foo", "BAR", "Java", "jAvA"})
void string_equals_ignore_case(String input) {
// The test logic is written once
assertTrue(input.equalsIgnoreCase(input.toLowerCase()));
}
}
Note that @ParameterizedTest and the argument sources live in the org.junit.jupiter.params package, which requires the junit-jupiter-params dependency in addition to the core JUnit Jupiter artifacts.
To run the test with Maven Surefire, from the project root:
mvn test -Dtest="StringEqualsIgnoreCaseTest"
With Gradle:
./gradlew test --tests "StringEqualsIgnoreCaseTest"
After execution, the Surefire XML (or HTML) report should show four test executions, each labelled with the parameter value (for example, string_equals_ignore_case_foo) thanks to the ReplaceUnderscores display name generator.
Trade-offs and limitations
- Complex argument sources: When you need to supply objects or multiple values per invocation, you must create a method annotated with
@MethodSourcethat returns aStream<Arguments>. This can increase class size and may require a customArgumentConverterfor complex types. - Potential explosion of invocations: Combining multiple sources can generate far more test cases than intended. Always verify the expected count in the test report.
- Instance lifecycle: If your test relies on shared state across invocations, you must explicitly set
@TestInstance(Lifecycle.PER_CLASS). Otherwise, each invocation gets a fresh instance, which may hide bugs that depend on state persistence. Conversely, avoid side effects between invocations; each parameterized test should be independent so one set of arguments cannot corrupt shared state for another.
Practical verification steps
- Run the test as shown above.
- Open the generated
target/surefire-reports/TEST-*.xml(Maven) orbuild/reports/tests/test/(Gradle) output. - Confirm that the number of
<testcase>elements matches the number of values you supplied. - Optionally, add a
@MethodSourcethat returns a known-size stream and confirm that the invocation count matches the stream size, ensuring the source is correctly wired.
Actionable closing
Start by identifying a test method that you have duplicated for different inputs. Replace it with a single @ParameterizedTest backed by the simplest appropriate source: @ValueSource for primitives or strings, @CsvSource for CSV-style data, or @MethodSource for custom objects. Run the test, inspect the report to verify the invocation count, and adjust the test instance lifecycle only if you truly need shared state. This approach keeps your test suite concise, readable, and easier to maintain as your data set grows.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.