Parameterized Tests in JUnit 5

2 minute read

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

  1. @ValueSource for simple single-parameter tests.
  2. @CsvSource for multi-parameter tests inline.
  3. @MethodSource for complex objects and logic-driven test data.
  4. @EnumSource to test all enum values automatically.
  5. Custom display names make test reports readable.

What’s Next?

Next: Nested and Repeated Tests — organize large test classes and detect flaky tests.

Updated: