Parameterized Test (@ParameterizedTest)

Unit tests frequently need to verify the same behavior with several input combinations. Writing a separate test method for every case creates duplication and makes suites difficult to maintain. JUnit 5’s @ParameterizedTest solves this by executing one test body multiple times with different arguments while preserving a separate result for each invocation.
Parameterized tests are provided by the junit-jupiter-params module. In a Maven project, add it with the following test dependency:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>

Spring Boot’s spring-boot-starter-test already includes this module in versions 2.2 and later, so no additional dependency is normally required in those projects.

Supplying Simple Values with @ValueSource

@ValueSource is the easiest way to test a method with a collection of literal values of the same type:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.assertTrue;

class StringUtilsTest {

    @ParameterizedTest
    @ValueSource(strings = {"racecar", "level", "rotator"})
    void detectsPalindromes(String candidate) {
        assertTrue(StringUtils.isPalindrome(candidate));
    }
}

JUnit creates one invocation for each string. If one value fails, the report identifies that specific invocation rather than treating the entire test as a single failure.

@ValueSource supports primitive values, strings, characters, booleans, and class literals. It can supply values such as ints, longs, doubles, chars, booleans, strings, or classes.

Combining Multiple Arguments

When the behavior under test accepts more than one parameter, @CsvSource provides a compact way to define argument sets:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculatorTest {

    @ParameterizedTest
    @CsvSource({
        "1, 1, 2",
        "2, 3, 5",
        "100, 200, 300",
        "-4, 9, 5"
    })
    void addsTwoNumbers(int first, int second, int expected) {
        assertEquals(expected, Calculator.add(first, second));
    }
}

Each row represents one invocation, and its values are converted to the parameter types declared by the test method.

For larger datasets, @CsvFileSource can load cases from a CSV file on the classpath:

@ParameterizedTest
@CsvFileSource(
    value = "/calculator-cases.csv",
    numLinesToSkip = 1
)
void addsNumbersFromFile(int first, int second, int expected) {
    assertEquals(expected, Calculator.add(first, second));
}

Here, numLinesToSkip = 1 ignores the header row. Keeping extensive data in a separate file can make it easier to review, reuse, and update without modifying test logic.

Creating Complex Arguments with @MethodSource

@MethodSource is useful when arguments require objects, calculated values, or custom construction. The referenced factory method may return a Stream, Collection, Iterator, or array of Arguments:

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 GreeterTest {

    @ParameterizedTest
    @MethodSource("users")
    void greetsUsers(User user, String expected) {
        assertEquals(expected, Greeter.greet(user));
    }

    static Stream<Arguments> users() {
        return Stream.of(
            Arguments.of(new User("Alice", 20), "Hello, Alice!"),
            Arguments.of(new User("Bob", 30), "Hello, Bob!")
        );
    }
}

The source method is normally static. It can instead be an instance method when the test class uses @TestInstance(TestInstance.Lifecycle.PER_CLASS). If the factory method has the same name as the parameterized test method, its name may be omitted from @MethodSource.

Testing Enum Constants

@EnumSource automatically generates arguments from the constants of an enum:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import java.util.concurrent.TimeUnit;

import static org.junit.jupiter.api.Assertions.assertNotNull;

class TimeUnitTest {

    @ParameterizedTest
    @EnumSource(
        value = TimeUnit.class,
        names = {"DAYS", "HOURS", "MINUTES"}
    )
    void supportsSelectedTimeUnits(TimeUnit unit) {
        assertNotNull(unit);
    }
}

The names attribute filters the constants included in the test. Omitting it runs the test against every enum constant, except those explicitly excluded.

Covering Empty and Null Inputs

Empty strings and null references are common boundary conditions. JUnit provides dedicated sources that can be combined with @ValueSource:

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "valid text"})
void acceptsExpectedText(String input) {
    assertTrue(Validator.isAcceptable(input));
}
  • @NullSource supplies a single null value.
  • @EmptySource supplies an empty string.
  • @NullAndEmptySource supplies both null and an empty string.
  • @ValueSource adds any number of explicit string values.

These annotations can be used together because each one contributes arguments to the same parameterized test.

Improving Test Reports

By default, parameterized invocations appear in test reports with an index and the supplied arguments. The name attribute can produce more meaningful descriptions:

@ParameterizedTest(name = "case {index}: {0} should be a palindrome")
@ValueSource(strings = {"racecar", "level", "rotator"})
void detectsPalindromes(String candidate) {
    assertTrue(StringUtils.isPalindrome(candidate));
}

The {index} placeholder represents the invocation number, while {0}, {1}, and later placeholders refer to individual arguments. The {arguments} placeholder displays all supplied values:

@ParameterizedTest(name = "{index} - {arguments}")
@CsvSource({
    "1, 2, 3",
    "10, 20, 30"
})
void addsNumbers(int first, int second, int expected) {
    assertEquals(expected, Calculator.add(first, second));
}

Readable names are especially valuable when a test fails, because they make the problematic data immediately visible.

Practical Guidelines

A well-designed parameterized test should follow several principles:

  • Separate data from behavior. Keep test cases in annotations, provider methods, or external files rather than adding conditional branches inside the test body.
  • Keep each test focused. All invocations of one parameterized method should verify the same behavior. Unrelated scenarios belong in separate test methods.
  • Include boundary cases. Add nulls, empty values, negative numbers, maximum limits, and invalid inputs where they are relevant.
  • Avoid excessive duplicate data. Repetition should expose meaningful variations rather than merely restating the same case.
  • Use appropriate sources. Choose @ValueSource for simple literals, @CsvSource for compact tables, and @MethodSource for objects or dynamically generated arguments.
  • Consider property-based testing for large input spaces. Frameworks such as jqwik can generate many combinations and check invariants that are difficult to enumerate manually.

Parameterized testing follows the principle of one behavior, many inputs. It reduces duplicated code, improves coverage, and makes failures easier to diagnose. By selecting the right argument source and adding descriptive invocation names, teams can turn repetitive test methods into concise, maintainable test suites.