Parameterized tests are one of the most powerful features in JUnit 6. Instead of writing ten near-identical test methods with different inputs, you write one test method and supply the inputs as a data source. JUnit 6 runs the method once per input row, reports each run individually, and gives you full control over how arguments are provided and displayed. This guide covers every argument source with complete code examples and expected output.
Setup: Adding the Dependency
Parameterized test support is included in junit-jupiter (the aggregator). If you added the aggregator, nothing extra is needed. If you added only junit-jupiter-api, add the params module separately:
<!-- Already included if you use the junit-jupiter aggregator -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>6.0.0</version>
<scope>test</scope>
</dependency>
1. @ValueSource — Single Values
The simplest source. Supplies a single argument of a primitive or String type per test invocation:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
class StringValidatorTest {
// Runs 4 times — once for each string in the array
@ParameterizedTest(name = "''{0}'' should not be blank")
@ValueSource(strings = {"Hello", "JUnit 6", "Java", " a "})
void nonBlankStringsAreValid(String input) {
assertFalse(input.isBlank(),
"'" + input + "' should not be blank");
}
// Works with int, long, double, boolean, char, byte, short, float, Class, URI, URL
@ParameterizedTest(name = "{0} is a positive number")
@ValueSource(ints = {1, 10, 100, Integer.MAX_VALUE})
void positiveIntegersAreGreaterThanZero(int number) {
assertTrue(number > 0);
}
}
Output:
✔ 'Hello' should not be blank
✔ 'JUnit 6' should not be blank
✔ 'Java' should not be blank
✔ ' a ' should not be blank
2. @NullSource, @EmptySource, @NullAndEmptySource
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.EmptySource;
import org.junit.jupiter.params.provider.NullAndEmptySource;
class EmailValidatorTest {
// @NullSource: supplies null as the argument
@ParameterizedTest
@NullSource
void nullEmailIsRejected(String email) {
assertThrows(IllegalArgumentException.class,
() -> emailValidator.validate(email));
}
// @EmptySource: supplies "" (empty string), empty list, empty array, etc.
@ParameterizedTest
@EmptySource
void emptyEmailIsRejected(String email) {
assertFalse(emailValidator.isValid(email));
}
// @NullAndEmptySource: combines both null AND empty in two invocations
@ParameterizedTest(name = "blank email ''{0}'' should be invalid")
@NullAndEmptySource
void blankEmailIsInvalid(String email) {
assertFalse(emailValidator.isValid(email));
}
// Combine @NullAndEmptySource with @ValueSource for comprehensive edge-case coverage
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "t", "n"})
void blankOrWhitespaceEmailIsInvalid(String email) {
assertFalse(emailValidator.isValid(email));
}
}
3. @CsvSource — Multiple Arguments Per Row
import org.junit.jupiter.params.provider.CsvSource;
class CalculatorTest {
// Each string is one row: "arg1, arg2, expectedResult"
// The test method receives 3 parameters per invocation
@ParameterizedTest(name = "{0} + {1} = {2}")
@CsvSource({
"1, 2, 3",
"10, 20, 30",
"-5, 5, 0",
"100, -50, 50"
})
void additionProducesCorrectResult(int addend1, int addend2, int expectedSum) {
Calculator calculator = new Calculator();
assertEquals(expectedSum, calculator.add(addend1, addend2),
addend1 + " + " + addend2 + " should equal " + expectedSum);
}
// Strings with commas: use single quotes to delimit them
@ParameterizedTest(name = "Formatting ''{0}'' with prefix ''{1}'' gives ''{2}''")
@CsvSource({
"hello, 'Mr.', 'Mr. hello'",
"world, 'Dr.', 'Dr. world'"
})
void formattingWithPrefixWorks(String name, String prefix, String expected) {
assertEquals(expected, formatter.format(prefix, name));
}
}
4. @CsvFileSource — Load Data from a CSV File
For large datasets, externalise test data into a CSV file under src/test/resources:
# src/test/resources/test-data/addition-cases.csv
# Lines starting with # are ignored (comments)
# addend1, addend2, expectedSum
1, 2, 3
10, 20, 30
-5, 5, 0
0, 0, 0
100, -50, 50
import org.junit.jupiter.params.provider.CsvFileSource;
class CalculatorFileTest {
@ParameterizedTest(name = "Row {index}: {0} + {1} = {2}")
@CsvFileSource(
resources = "/test-data/addition-cases.csv",
numLinesToSkip = 3 // skip the 3 comment/header lines
)
void additionFromCsvFile(int addend1, int addend2, int expectedSum) {
assertEquals(expectedSum, new Calculator().add(addend1, addend2));
}
}
5. @MethodSource — Arguments from a Factory Method
For complex objects or programmatically generated test data, provide a static factory method that returns a Stream:
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import org.junit.jupiter.params.provider.Arguments;
class OrderValidationTest {
// Factory method: must be static and return a Stream
static Stream invalidOrderCases() {
return Stream.of(
// Arguments.of(input, expectedExceptionMessage)
Arguments.of(null, "Order must not be null"),
Arguments.of(new Order(null, 10.0), "Customer email is required"),
Arguments.of(new Order("[email protected]", -1.0), "Order total must be positive"),
Arguments.of(new Order("[email protected]", 0.0), "Order total must be positive")
);
}
// @MethodSource references the factory method by name
@ParameterizedTest(name = "Invalid order: {1}")
@MethodSource("invalidOrderCases")
void invalidOrdersAreRejected(Order invalidOrder, String expectedMessage) {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> orderValidator.validate(invalidOrder)
);
assertEquals(expectedMessage, exception.getMessage());
}
}
6. @EnumSource — All or Selected Enum Values
import org.junit.jupiter.params.provider.EnumSource;
enum OrderStatus { PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED }
class OrderStatusTest {
// Runs once for EVERY enum value
@ParameterizedTest(name = "Order status {0} has a non-null display name")
@EnumSource(OrderStatus.class)
void everyStatusHasNonNullDisplayName(OrderStatus status) {
assertNotNull(status.name(), "Every OrderStatus must have a name");
}
// Include only specific values
@ParameterizedTest(name = "Active status: {0}")
@EnumSource(value = OrderStatus.class, names = {"PENDING", "PROCESSING", "SHIPPED"})
void activeStatusesAreNotTerminal(OrderStatus activeStatus) {
assertFalse(orderService.isTerminalStatus(activeStatus));
}
// Exclude specific values using EXCLUDE mode
@ParameterizedTest
@EnumSource(value = OrderStatus.class,
names = {"DELIVERED", "CANCELLED"},
mode = EnumSource.Mode.EXCLUDE)
void nonTerminalStatusesCanTransitionToShipped(OrderStatus status) {
assertTrue(orderService.canTransitionTo(status, OrderStatus.SHIPPED));
}
}
7. @ArgumentsSource — Custom Argument Provider
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.jupiter.extension.ExtensionContext;
import java.util.stream.Stream;
// Custom provider: reads test data from a database or external source
public class DatabaseOrderArgumentsProvider implements ArgumentsProvider {
@Override
public Stream provideArguments(ExtensionContext context) {
// Could read from a database, API, file, etc.
return Stream.of(
Arguments.of(new Order("[email protected]", 50.00), OrderStatus.PENDING),
Arguments.of(new Order("[email protected]", 200.00), OrderStatus.PENDING)
);
}
}
class OrderCreationTest {
@ParameterizedTest
@ArgumentsSource(DatabaseOrderArgumentsProvider.class)
void newlyCreatedOrderHasPendingStatus(Order order, OrderStatus expectedStatus) {
Order created = orderService.create(order);
assertEquals(expectedStatus, created.getStatus());
}
}
Argument Sources Quick Reference
| Source | Best for | Multi-arg? |
|---|---|---|
@ValueSource | Single primitive/String values | No |
@NullSource | null edge case | No |
@EmptySource | empty string/collection edge case | No |
@NullAndEmptySource | Both null and empty | No |
@CsvSource | Multiple args inline | Yes |
@CsvFileSource | Large datasets in CSV files | Yes |
@MethodSource | Complex objects, programmatic data | Yes |
@EnumSource | Enum values (all or subset) | No |
@ArgumentsSource | Custom / dynamic data providers | Yes |
Frequently Asked Questions (FAQs)
Q1: Can I combine multiple argument sources on one test?
Yes. You can stack multiple source annotations on a single @ParameterizedTest method. For example, @NullAndEmptySource combined with @ValueSource provides null, empty, and the specified values in sequence. JUnit 6 runs the test once for each value from each source in the order they are declared.
Q2: How do I give each parameterized test invocation a custom display name?
Use the name attribute of @ParameterizedTest. Placeholders include: {index} (1-based invocation count), {0}, {1}, etc. (argument values by position), and {arguments} (all arguments comma-separated). Example: @ParameterizedTest(name = "[{index}] {0} + {1} = {2}").
Q3: Can @MethodSource reference a method in another class?
Yes. Use the fully qualified class name: @MethodSource("com.example.TestData#orderCases"). This is useful for sharing test data factories across multiple test classes. The method must still be static and return a Stream, Iterable, Iterator, or array.
Q4: Does each @ParameterizedTest invocation count as a separate test?
Yes. Each invocation is reported individually in IDE test panels and build reports. If one invocation fails, the others still run. This is one of the key advantages over loops — you get a full picture of which inputs pass and which fail in a single test run.
Q5: Can I use @ParameterizedTest with @Nested classes?
Yes. @ParameterizedTest works inside @Nested classes exactly like regular @Test methods. The outer class’s @BeforeEach still runs before each parameterized invocation. This is a powerful combination for scenario-based parameterized testing: see JUnit 6 Nested Tests for patterns.
See Also
- JUnit 6 Tutorial: Complete Series Index
- JUnit 6 Assertions: All Methods Explained
- Dynamic Tests in JUnit 6 using @TestFactory
- Structuring Tests with JUnit 6 Nested Tests
- Tags and Test Suites in JUnit 6
Conclusion
Parameterized tests eliminate duplication, dramatically increase coverage, and make edge-case testing natural rather than tedious. Use @ValueSource for quick single-argument cases, @CsvSource for multi-argument inline data, @MethodSource for complex objects, and @CsvFileSource when your dataset is large enough to live in a file. Every case you cover here reduces the chance of a production bug slipping through.
Next: Dynamic Tests in JUnit 6 using @TestFactory — generate tests at runtime based on data that isn’t known until the test executes.