Parameterized Tests in JUnit 5
Instead of writing 10 nearly identical tests, write one test and feed it different inputs. Parameterized tests reduce duplication and improve coverage.
Maven Dependency
Included in junit-jupiter, but if using modules separately:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.10.3</version>
<scope>test</scope>
</dependency>
@ValueSource — Simple Values
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@ParameterizedTest
@ValueSource(strings = {"racecar", "madam", "level", "kayak"})
void shouldDetectPalindromes(String word) {
assertTrue(StringUtils.isPalindrome(word));
}
@ParameterizedTest
@ValueSource(ints = {2, 4, 6, 8, 100})
void shouldBeEven(int number) {
assertEquals(0, number % 2);
}
@ParameterizedTest
@ValueSource(strings = {"", " ", "\t", "\n"})
void shouldDetectBlankStrings(String input) {
assertTrue(input.isBlank());
}
Supported Types
strings, ints, longs, doubles, floats, bytes, shorts, chars, booleans, classes
@NullSource, @EmptySource, @NullAndEmptySource
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\t"})
void shouldRejectInvalidInput(String input) {
assertThrows(IllegalArgumentException.class,
() -> userService.createUser(input));
}
@CsvSource — Multiple Parameters
@ParameterizedTest
@CsvSource({
"hello, olleh",
"Java, avaJ",
"abc, cba",
"'', ''"
})
void shouldReverseStrings(String input, String expected) {
assertEquals(expected.trim(), StringUtils.reverse(input));
}
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"5, 3, 8",
"0, 0, 0",
"-1, 1, 0"
})
void shouldAddNumbers(int a, int b, int expected) {
assertEquals(expected, a + b);
}
@CsvFileSource — External CSV
src/test/resources/test-data/emails.csv:
email,valid
alice@example.com,true
bob@test.co.uk,true
invalid-email,false
@missing.com,false
user@,false
@ParameterizedTest
@CsvFileSource(resources = "/test-data/emails.csv", numLinesToSkip = 1)
void shouldValidateEmails(String email, boolean valid) {
assertEquals(valid, EmailValidator.isValid(email));
}
@MethodSource — Complex Arguments
@ParameterizedTest
@MethodSource("provideUsersForValidation")
void shouldValidateUser(String name, String email, boolean expectedValid) {
User user = new User(name, email);
assertEquals(expectedValid, userValidator.isValid(user));
}
static Stream<Arguments> provideUsersForValidation() {
return Stream.of(
Arguments.of("Alice", "alice@example.com", true),
Arguments.of("", "alice@example.com", false),
Arguments.of("Alice", "", false),
Arguments.of(null, "alice@example.com", false),
Arguments.of("Alice", "not-an-email", false)
);
}
External Method Source
@ParameterizedTest
@MethodSource("com.myapp.TestDataFactory#validEmails")
void shouldAcceptValidEmails(String email) {
assertTrue(EmailValidator.isValid(email));
}
@EnumSource — Test All Enum Values
enum Status { PENDING, ACTIVE, SUSPENDED, DELETED }
@ParameterizedTest
@EnumSource(Status.class)
void shouldHandleAllStatuses(Status status) {
assertDoesNotThrow(() -> statusHandler.process(status));
}
// Exclude specific values
@ParameterizedTest
@EnumSource(value = Status.class, names = {"DELETED"}, mode = EnumSource.Mode.EXCLUDE)
void shouldAllowTransitionForNonDeleted(Status status) {
assertTrue(statusHandler.canTransition(status));
}
Custom Display Names
@ParameterizedTest(name = "{index}: isPalindrome(\"{0}\") = {1}")
@CsvSource({
"racecar, true",
"hello, false",
"madam, true",
"java, false"
})
void palindromeCheck(String word, boolean expected) {
assertEquals(expected, StringUtils.isPalindrome(word));
}
Output:
1: isPalindrome("racecar") = true ✓
2: isPalindrome("hello") = false ✓
3: isPalindrome("madam") = true ✓
4: isPalindrome("java") = false ✓
Placeholders
| Placeholder | Value |
|---|---|
{index} |
1-based invocation index |
{0}, {1} |
Method parameters |
{displayName} |
Test display name |
{arguments} |
All arguments |
@ArgumentsSource — Custom Provider
class PrimeNumberProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of(2, 3, 5, 7, 11, 13, 17, 19, 23)
.map(Arguments::of);
}
}
@ParameterizedTest
@ArgumentsSource(PrimeNumberProvider.class)
void shouldBePrime(int number) {
assertTrue(MathUtils.isPrime(number));
}
Real-World Example: Testing a Price Calculator
@ParameterizedTest(name = "quantity={0}, unitPrice={1}, discount={2}% → total={3}")
@CsvSource({
"1, 100.00, 0, 100.00",
"5, 20.00, 0, 100.00",
"10, 50.00, 10, 450.00",
"3, 33.33, 25, 74.99",
"0, 100.00, 0, 0.00"
})
void shouldCalculateTotal(int quantity, double unitPrice,
int discountPercent, double expectedTotal) {
double actual = priceCalculator.calculateTotal(quantity, unitPrice, discountPercent);
assertEquals(expectedTotal, actual, 0.01);
}
Common Mistakes
| Mistake | Fix |
|---|---|
@MethodSource method not static |
Must be static (or use PER_CLASS lifecycle) |
| Method source name doesn’t match | Default is same name as test; explicit: @MethodSource("methodName") |
| CSV column count mismatch | Ensure CSV columns match method parameters |
Using @Test instead of @ParameterizedTest |
Replace @Test — they’re different annotations |
Key Takeaways
@ValueSourcefor simple single-parameter tests.@CsvSourcefor multi-parameter tests inline.@MethodSourcefor complex objects and logic-driven test data.@EnumSourceto test all enum values automatically.- Custom display names make test reports readable.
What’s Next?
Next: Nested and Repeated Tests — organize large test classes and detect flaky tests.