@CsvSource and @CsvFileSource work well when your data is flat — strings, numbers, booleans. The moment you need to pass a fully constructed object as a parameter — a User with five fields, an HttpRequest with headers — CSV cannot represent it. @MethodSource is the answer: reference a factory method that builds and returns whatever types your test needs. It is the JUnit 5 equivalent of TestNG's @DataProvider, and it is the most flexible source annotation.
The basic pattern
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.Arguments;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
class UserServiceTest {
@ParameterizedTest
@MethodSource("loginTestData")
void testLogin(String email, String password, int expectedStatus) {
Response response = apiClient.login(email, password);
assertEquals(expectedStatus, response.getStatusCode());
}
static Stream<Arguments> loginTestData() {
return Stream.of(
Arguments.of("admin@test.com", "AdminPass123", 200),
Arguments.of("user@test.com", "UserPass123", 200),
Arguments.of("wrong@test.com", "BadPass", 401),
Arguments.of("", "password", 400),
Arguments.of(null, "password", 400)
);
}
}The factory method must be static by default (or non-static with @TestInstance(Lifecycle.PER_CLASS)). It returns Stream<Arguments> — each Arguments.of(...) becomes one test invocation. Arguments.of accepts any number of values of any type.
Passing complex objects
This is where @MethodSource outshines CSV-based sources. You can construct real objects with builders, set specific fields, and control edge cases precisely:
@ParameterizedTest
@MethodSource("userCreationScenarios")
void testCreateUser(User user, int expectedStatus, String expectedError) {
Response response = apiClient.createUser(user);
assertEquals(expectedStatus, response.getStatusCode());
if (expectedError != null) {
assertEquals(expectedError, response.jsonPath().getString("error"));
}
}
static Stream<Arguments> userCreationScenarios() {
return Stream.of(
Arguments.of(
new User("Alice", "alice@test.com", "admin"),
201, null
),
Arguments.of(
new User("", "bob@test.com", "tester"),
400, "Name cannot be empty"
),
Arguments.of(
new User("Charlie", "", "tester"),
400, "Email cannot be empty"
),
Arguments.of(
new User("Dave", "not-an-email", "tester"),
400, "Email format is invalid"
)
);
}No CSV can represent new User(...). @MethodSource can. This mirrors TestNG's @DataProvider returning Object[][] with pre-built objects — the difference is that JUnit's factory method returns a typed Stream rather than an untyped array.
Implicit method name matching
When the factory method has the same name as the test method, you can omit the value from @MethodSource:
@ParameterizedTest
@MethodSource // looks for: static Stream<Arguments> testLogin()
void testLogin(String email, String password) { ... }
static Stream<Arguments> testLogin() {
return Stream.of(
Arguments.of("admin@test.com", "AdminPass123"),
Arguments.of("user@test.com", "UserPass123")
);
}This is a convention for keeping the factory close to its test. It works cleanly for small test classes. In larger test suites with shared data, an explicit name is clearer.
Factory method in an external class
Shared test data should live in one place. Put the factory in a dedicated data class and reference it by fully qualified method name:
// src/test/java/com/mycompany/testdata/LoginData.java
package com.mycompany.testdata;
public class LoginData {
public static Stream<Arguments> validCredentials() {
return Stream.of(
Arguments.of("admin@test.com", "AdminPass123", 200),
Arguments.of("user@test.com", "UserPass123", 200)
);
}
public static Stream<Arguments> invalidCredentials() {
return Stream.of(
Arguments.of("wrong@test.com", "BadPass", 401),
Arguments.of("", "password", 400)
);
}
}Reference them in tests across the project:
@ParameterizedTest
@MethodSource("com.mycompany.testdata.LoginData#validCredentials")
void validLoginReturns200(String email, String password, int expected) { ... }
@ParameterizedTest
@MethodSource("com.mycompany.testdata.LoginData#invalidCredentials")
void invalidLoginIsRejected(String email, String password, int expected) { ... }In TestNG, a @DataProvider in a separate class requires dataProviderClass = LoginData.class. The JUnit equivalent is the fully qualified ClassName#methodName string. Both require the method to be static.
Single-parameter shorthand
When the test has only one parameter, the factory can return a Stream of values directly — no Arguments.of wrapping needed:
@ParameterizedTest
@MethodSource("validEmails")
void shouldAcceptValidEmail(String email) {
assertTrue(EmailValidator.isValid(email));
}
static Stream<String> validEmails() {
return Stream.of(
"alice@test.com",
"bob@domain.org",
"charlie@example.co.uk"
);
}For single-parameter tests you can use @ValueSource instead — @MethodSource becomes worth it when you need to compute the values (read from a file, query a database, call a builder).
When to use which source
| Scenario | Best source |
|---|---|
| A handful of simple strings or ints | @ValueSource |
| Null/empty boundary cases | @NullAndEmptySource |
| Multi-column data, 3–15 rows | @CsvSource |
| Multi-column data, 20+ rows | @CsvFileSource |
| Complex objects (User, HttpRequest) | @MethodSource |
| Computed values (database, API) | @MethodSource |
| Every constant of an enum | @EnumSource |
@MethodSource data flow
⚠️ Common mistakes
- Non-static factory method without
@TestInstance(PER_CLASS).@MethodSourcecalls the factory before any test instance is created. WithoutPER_CLASS, JUnit cannot call an instance method. The error isorg.junit.jupiter.api.extension.ExtensionConfigurationException: ... must be static unless.... Addstaticto the factory, or add@TestInstance(Lifecycle.PER_CLASS)to the test class. - Factory returns
nullor an empty stream. IfloginTestData()returnsStream.empty(), JUnit runs zero tests with no warning — the test simply disappears from the report. If you see a test method name in the report with 0 executions, check that the factory returns at least oneArguments.of(...). - Misspelling the factory method name in
@MethodSource.@MethodSource("loginTestdata")(lowercased) fails at runtime withCould not find factory method [loginTestdata]. The name is case-sensitive. Use the implicit-name form (no string) when factory and test have the same name — eliminates the typo risk entirely.
🎯 Practice task
Replace a @DataProvider pattern with @MethodSource. 25–35 minutes.
- Create a
ProductServicewith a methodapplyDiscount(Product product, double percent)that returns adouble. ThrowIllegalArgumentExceptionfor negative percentages or null products. - Write
ProductServiceTestwith:- A
@MethodSource("discountScenarios")test that passesProductobjects and percentages — assert the expected discounted price within0.01delta. - A
@MethodSource("invalidDiscountScenarios")test that assertsassertThrowsfor each invalid input.
- A
- Move the factory methods to an external
ProductTestDataclass. Use the fully qualified"com.mycompany.testdata.ProductTestData#discountScenarios"reference. - Single-parameter shorthand. Write a
@MethodSource("productNames")test using a factory that returnsStream<String>(notStream<Arguments>). Confirm the test receives a plainStringparameter. - Stretch — computed data. In the factory, build your test data programmatically: use
IntStream.rangeClosed(1, 5).mapToObj(i -> Arguments.of(new Product("Product " + i, i * 10.0), 10.0, i * 9.0))to generate five products in a loop. Confirm five tests run.
Next lesson: @RepeatedTest — running a test multiple times to detect flakiness and measure consistency.