Appearance
JUnit 5: Parameterized Tests (Part 2)
The Problem with Simple Types
In Part 1 we covered how to supply strings, integers, enums, and CSV rows to parameterized tests. That handles a large portion of real world scenarios. But what happens when the object under test is more complex?
Consider a user registration service. Testing it thoroughly requires feeding a User object that has a name, age, country, and status. You cannot express a User object with @ValueSource(strings = {...}). You need a mechanism that either constructs the object for you or accepts an already-constructed one.
This chapter covers three advanced source strategies for objects and complex parameters: @MethodSource, @ArgumentsSource, and the argument aggregator pattern. It also covers ArgumentsAccessor for accessing raw multi column data, plus explicit aggregation with @AggregateWith.
@MethodSource — Test Data from a Static Method
@MethodSource points to a static method in the test class (or in another class) whose return value provides the stream of test data.
Return Types and Their Meaning
| Return type | Use case |
|---|---|
Stream<T> | Single parameter, any object type |
Stream<Arguments> | Multiple parameters |
Collection<T> (e.g. List<T>) | Single parameter |
T[] (e.g. String[]) | Single parameter |
Object[][] | Multiple parameters |
Iterator<T> | Single parameter |
Single Object Parameter
java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PalindromeMethodSourceTest {
/**
* When no method name is supplied to @MethodSource,
* JUnit 5 looks for a static method whose name exactly matches
* the test method name: "testPalindrome".
*/
@ParameterizedTest
@MethodSource
void testPalindrome(String word) {
assertTrue(StringUtil.isPalindrome(word));
}
// Implicit resolution: same name as the test method
static Stream<String> testPalindrome() {
return Stream.of("civic", "level", "madam");
}
}You may also supply an explicit method name:
java
@ParameterizedTest
@MethodSource("palindromeData")
void testPalindromeExplicit(String word) {
assertTrue(StringUtil.isPalindrome(word));
}
static Stream<String> palindromeData() {
return Stream.of("civic", "radar", "noon");
}Passing Object Parameters
If your test method accepts a domain object, return a stream of those objects from the provider method:
java
class UserValidationTest {
@ParameterizedTest
@MethodSource("validUsers")
void testValidUser(User user) {
assertNotNull(user.getName());
assertTrue(user.getAge() > 0);
}
static Stream<User> validUsers() {
return Stream.of(
new User("Alice", 30, "India", "ACTIVE"),
new User("Bob", 25, "Russia", "INACTIVE")
);
}
}Multiple Parameters via Stream<Arguments>
When your test method accepts multiple parameters, the provider method returns Stream<Arguments>. Each Arguments.of(...) call corresponds to one test run:
java
import org.junit.jupiter.params.provider.Arguments;
import static org.junit.jupiter.params.provider.Arguments.arguments;
class CalculatorMethodSourceTest {
@ParameterizedTest
@MethodSource("additionData")
void testAddition(int a, int b, int expected) {
assertEquals(expected, a + b);
}
static Stream<Arguments> additionData() {
return Stream.of(
arguments(1, 2, 3),
arguments(5, 5, 10),
arguments(10, 20, 30)
);
}
}Provider Method in a Different Class
If the static provider method lives in a separate class, supply the fully qualified reference using ClassName#methodName:
java
@ParameterizedTest
@MethodSource("com.example.TestData#palindromeData")
void testPalindromeExternalData(String word) {
assertTrue(StringUtil.isPalindrome(word));
}java
// In a different file: com/example/TestData.java
package com.example;
import java.util.stream.Stream;
public class TestData {
public static Stream<String> palindromeData() {
return Stream.of("civic", "level", "noon");
}
}@ArgumentsSource — Test Data from a Dedicated Class
@ArgumentsSource is conceptually similar to @MethodSource, but instead of a static method, you create a dedicated class that implements the ArgumentsProvider interface. This class can be reused across multiple test classes and has access to the ExtensionContext, which carries full information about the test class, method, and annotations.
java
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import java.util.stream.Stream;
import static org.junit.jupiter.params.provider.Arguments.arguments;
class UserRegistrationTest {
@ParameterizedTest
@ArgumentsSource(UserArgumentsProvider.class)
void testUserRegistration(User user) {
assertNotNull(user);
assertNotNull(user.getName());
}
}
/**
* A reusable provider class.
* The return type MUST always be Stream<Arguments>, regardless of whether
* the test uses one parameter or many.
*/
class UserArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of(
arguments(new User("Alice", 30, "India", "ACTIVE")),
arguments(new User("Bob", 25, "Russia", "INACTIVE"))
);
}
}Key difference from @MethodSource:
@MethodSource | @ArgumentsSource | |
|---|---|---|
| Data lives in | Static method (same or different class) | Separate class implementing ArgumentsProvider |
| Return type flexibility | Many options | Always Stream<Arguments> |
| Access to test context | No | Yes (ExtensionContext) |
| Reusability | Good | Excellent |
ArgumentsAccessor — Accessing Raw multi column Data by Index
When a test method has many parameters (say five or six), the method signature becomes unwieldy. ArgumentsAccessor provides an alternative: accept a single ArgumentsAccessor parameter and access individual values by index.
java
import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
class UserCreationTest {
/**
* Instead of declaring (String name, int age, String country, String status),
* we accept a single ArgumentsAccessor and read columns by index.
*/
@ParameterizedTest
@CsvSource({
"Alice, 30, India, ACTIVE",
"Bob, 25, Russia, INACTIVE"
})
void testCreateUser(ArgumentsAccessor accessor) {
String name = accessor.getString(0);
int age = accessor.getInteger(1);
String country = accessor.getString(2);
String status = accessor.getString(3);
User user = new User(name, age, country, status);
assertNotNull(user.getName());
assertTrue(user.getAge() > 0);
}
}ArgumentsAccessor methods:
getString(int index)— retrieves the value as aStringgetInteger(int index)— retrieves asintgetLong(int index)/getDouble(int index)— other primitive wrappersget(int index, Class<T> type)— typed retrieval
This keeps your test method signature clean while still letting you work with every column of a multi parameter row.
@AggregateWith — Explicit Aggregation for Multiple Parameters
In Part 1, @ConvertWith was introduced for explicit single parameter conversion. The multi parameter equivalent is @AggregateWith. It accepts a class implementing ArgumentsAggregator and is applied to one parameter that will receive the fully assembled object.
Execution Sequence
@CsvSource(or any other source) provides raw string data for a given run.@AggregateWithtriggers theArgumentsAggregatorclass.- Inside the aggregator,
ArgumentsAccessoris used to read individual columns. - The aggregator returns the assembled object, which is injected into the annotated parameter.
- Test body executes.
java
import org.junit.jupiter.params.aggregator.AggregateWith;
import org.junit.jupiter.params.aggregator.ArgumentsAggregationException;
import org.junit.jupiter.params.aggregator.ArgumentsAggregator;
class UserAggregationTest {
/**
* The @AggregateWith annotation tells JUnit to route all CSV columns
* through UserAggregator before injecting them as a User object.
*/
@ParameterizedTest
@CsvSource({
"Alice, 30, India, ACTIVE",
"Bob, 25, Russia, INACTIVE"
})
void testUserFromCsv(@AggregateWith(UserAggregator.class) User user) {
assertNotNull(user.getName());
assertTrue(user.getAge() > 0);
}
}
class UserAggregator implements ArgumentsAggregator {
@Override
public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context)
throws ArgumentsAggregationException {
return new User(
accessor.getString(0), // name
accessor.getInteger(1), // age
accessor.getString(2), // country
accessor.getString(3) // status
);
}
}Comparison: @ConvertWith vs @AggregateWith
| Feature | @ConvertWith | @AggregateWith |
|---|---|---|
| Parameter count | Single | Multiple (aggregates into one object) |
| Interface to implement | ArgumentConverter | ArgumentsAggregator |
| Input | One raw value | ArgumentsAccessor with all columns |
| Typical use | Type coercion (String to int) | Object construction from CSV columns |
Complete Example: All Techniques Together
Below is a realistic scenario where a UserService.register() method is tested with various techniques:
java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.aggregator.AggregateWith;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.params.provider.Arguments.arguments;
class UserServiceTest {
private final UserService service = new UserService();
// ---- Using @MethodSource for object parameters ----
@ParameterizedTest(name = "Test {index}: register {0}")
@MethodSource("userProvider")
void testRegisterUser(User user) {
User registered = service.register(user);
assertNotNull(registered.getId(), "Registered user must have an ID");
}
static Stream<org.junit.jupiter.params.provider.Arguments> userProvider() {
return Stream.of(
arguments(new User("Alice", 30, "India", "ACTIVE")),
arguments(new User("Bob", 22, "Russia", "ACTIVE"))
);
}
// ---- Using @AggregateWith for clean method signature ----
@ParameterizedTest(name = "Test {index}: {0} age={1}")
@CsvSource({
"Carol, 28, Germany, ACTIVE",
"Dave, 35, Japan, INACTIVE"
})
void testRegisterFromCsv(@AggregateWith(UserAggregator.class) User user) {
assertNotNull(user.getName());
assertTrue(user.getAge() >= 18, "User must be an adult");
}
}Choosing the Right Source
| Situation | Recommended source |
|---|---|
| Simple values (strings, ints) | @ValueSource |
| Enum constants | @EnumSource |
| Multiple primitive columns, small dataset | @CsvSource |
| Multiple primitive columns, large dataset | @CsvFileSource |
| Object parameters from same test class | @MethodSource (static method) |
| Object parameters in shared utility class | @MethodSource("ClassName#method") |
| Reusable provider with context access | @ArgumentsSource |
| Clean method signature with many CSV columns | @AggregateWith |
Interview Questions & Pitfalls
Q1. What is the difference between @MethodSource and @ArgumentsSource?
@MethodSource reads test data from a static method. @ArgumentsSource reads from a dedicated class implementing ArgumentsProvider. The class approach is more reusable and provides access to ExtensionContext (test class, method, annotations). The static method approach is simpler for local data. Both are valid in practice.
Q2. When would you use ArgumentsAccessor instead of declaring all parameters explicitly in the method signature?
When a test method would need many parameters (four, five, or more), declaring them all in the signature becomes hard to read. ArgumentsAccessor collapses all values into one object and lets you retrieve them by index. This is especially useful when the values are later used to construct a domain object.
Q3. What is the difference between @ConvertWith and @AggregateWith?
@ConvertWith takes a single raw value and converts it (e.g., string to uppercase). It implements ArgumentConverter and works only for single parameter scenarios. @AggregateWith receives all columns via ArgumentsAccessor and assembles them into one object. It implements ArgumentsAggregator and is used for multi parameter scenarios.
Q4. What return type must ArgumentsProvider.provideArguments() return?
It must always return Stream<? extends Arguments>. Unlike @MethodSource, which supports many return type variations, @ArgumentsSource has one fixed return type. This simplicity is a design trade off for the consistency it provides.
Q5. How do you reference a static provider method in a different class using @MethodSource?
Use the fully qualified class name followed by # and the method name: @MethodSource("com.example.TestData#palindromeData"). This is often used to share test data across multiple test classes without duplication.
Q6. Can you pass an object parameter using @ValueSource?
No. @ValueSource only supports primitive types and String. For object parameters you must use @MethodSource, @ArgumentsSource, or @CsvSource combined with @AggregateWith.
Q7. What happens if @MethodSource is used without specifying a method name and no static method matches the test method name?
JUnit 5 throws a PreconditionViolationException at runtime stating that it could not find a suitable factory method. Always ensure either the names match exactly (for implicit resolution) or an explicit name is provided.