When would you implement a custom JUnit 5 ArgumentsProvider and wire it with @ArgumentsSource instead of using @MethodSource, and how do you make such a provider configurable through your own annotation?
answer
- @ArgumentsSource(Provider.class) — the SPI behind @ValueSource/@CsvSource
- Provider: no-arg ctor, top-level or static nested
- provideArguments(ExtensionContext) → Stream<? extends Arguments>
- AnnotationConsumer<MyAnn>.accept() runs before provideArguments
- Use for generated/discovered/context-aware data; else @MethodSource
basics
~20 sWrite an ArgumentsProvider when the data source is reusable logic rather than a list — generated, loaded from files, or driven by ExtensionContext. Wire it with @ArgumentsSource(MyProvider.class). Make it configurable by having the provider implement AnnotationConsumer<MyAnnotation> and putting @ArgumentsSource on your own annotation.
solid answer
~50 s`@MethodSource` covers most cases: a factory method next to the test. Reach for `ArgumentsProvider` when the *source itself* is a reusable component — generating randomized or exhaustive cases, scanning a directory of golden files, deriving cases from `ExtensionContext` (the test class, tags, store), or a fixture shared across projects. A provider implements `ArgumentsProvider` and returns a `Stream<? extends Arguments>`; wire it with `@ArgumentsSource(GoldenFilesProvider.class)`. It needs a no-arg constructor and must be a top-level or static nested class. To make it configurable, add your own annotation carrying the parameters, meta-annotate it with `@ArgumentsSource`, and have the provider implement `AnnotationConsumer<YourAnnotation>`; JUnit calls `accept(annotation)` before providing, so the provider can read the values: ```java @ParameterizedTest @GoldenFiles(dir = "/golden/json", extension = ".json") void roundTrips(Path file) { ... } ``` That is exactly how JUnit's own `@ValueSource` and `@CsvSource` are built — each is an annotation plus a provider.
code
java · 29 lines@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@ParameterizedTest
@ArgumentsSource(GoldenFilesProvider.class)
public @interface GoldenFiles {
String dir();
String extension() default ".json";
}
public class GoldenFilesProvider
implements ArgumentsProvider, AnnotationConsumer<GoldenFiles> {
private String dir;
private String extension;
@Override
public void accept(GoldenFiles annotation) {
this.dir = annotation.dir();
this.extension = annotation.extension();
}
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
return Files.list(Path.of("src/test/resources", dir))
.filter(p -> p.toString().endsWith(extension))
.sorted()
.map(Arguments::of);
}
}go deeper
It is enough to know @ArgumentsSource exists and that it points at a class implementing ArgumentsProvider.
Write a provider correctly — no-arg constructor, static nested or top-level, returning Stream<? extends Arguments> — and know when @MethodSource is simpler.
Lead with the criteria for choosing it (reusable, context-aware, generated/discovered data) and demonstrate the custom annotation plus AnnotationConsumer pattern, including determinism concerns.
Frame it as building a small internal testing API: an annotation is a contract other teams will read, so naming, defaults, failure-on-empty behaviour and reproducibility are design decisions, not details.
## The mechanism behind every argument source `@ValueSource`, `@EnumSource`, `@CsvSource`, `@MethodSource` are not special-cased by the engine. Each is an annotation meta-annotated with `@ArgumentsSource(SomeProvider.class)`, and each provider implements the same `ArgumentsProvider` SPI. So `@ArgumentsSource` is not an exotic corner — it is the base mechanism, and the built-in sources are its first customers. ```java public class GoldenFilesProvider implements ArgumentsProvider { @Override public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception { Path dir = Path.of("src/test/resources/golden"); return Files.list(dir).filter(p -> p.toString().endsWith(".json")).map(Arguments::of); } } @ParameterizedTest @ArgumentsSource(GoldenFilesProvider.class) void roundTrips(Path file) { ... } ``` Requirements on the class: a **no-arg constructor**, and it must be **top-level or a static nested class** (JUnit instantiates it reflectively — it cannot construct an inner class bound to an enclosing instance). ## When it beats @MethodSource A static factory method is simpler and should be your default. A provider earns its keep when at least one of these is true: **1. The source is reusable across many tests or projects.** A factory can be shared via `FQCN#method`, but a provider plus a custom annotation gives consumers a *declarative* API: `@GoldenFiles(dir = "...")` instead of a fully qualified string. That reads better and survives fixture-class moves. **2. The provider needs context.** `provideArguments` receives the `ExtensionContext`: the test class and method, display name, tags, configuration parameters, and the extension `Store`. A provider can therefore vary its data by tag (a small set locally, an exhaustive set when a `@Tag("nightly")` is present), read a JUnit configuration parameter, or pull a fixture another extension put in the store. A plain factory method sees none of that. **3. The data is generated or discovered.** Enumerating a directory of golden files, expanding a combinatorial matrix, walking a schema, streaming from a generator with a seeded `Random` — logic that deserves its own class, its own tests, and its own name. **4. You want to hide configuration behind an annotation.** See below. If none of these hold, `@MethodSource` is the right answer, and proposing a provider for three literal rows is over-engineering an interviewer will notice. ## Making a provider configurable: AnnotationConsumer Hard-coding a path inside a provider makes it single-purpose. The idiomatic fix is a **custom composed annotation** plus `AnnotationConsumer`: ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @ArgumentsSource(GoldenFilesProvider.class) public @interface GoldenFiles { String dir(); String extension() default ".json"; } public class GoldenFilesProvider implements ArgumentsProvider, AnnotationConsumer<GoldenFiles> { private String dir; private String extension; @Override public void accept(GoldenFiles annotation) { this.dir = annotation.dir(); this.extension = annotation.extension(); } @Override public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception { Path root = Path.of("src/test/resources", dir); return Files.list(root) .filter(p -> p.toString().endsWith(extension)) .sorted() .map(Arguments::of); } } ``` JUnit sees `@GoldenFiles` on the test method, finds the meta-annotation `@ArgumentsSource`, instantiates the provider, and — because it implements `AnnotationConsumer<GoldenFiles>` — calls `accept(theAnnotationInstance)` **before** `provideArguments`. The provider is a fresh instance per test method, so storing configuration in fields is safe. The test then reads as domain language: ```java @ParameterizedTest @GoldenFiles(dir = "golden/orders", extension = ".json") void roundTripsGoldenOrders(Path file) { ... } ``` Adding `@ParameterizedTest` itself to the composed annotation collapses it further to a single line on the test. ## Operational notes - **Return `Stream<? extends Arguments>`**, one element per invocation; use `Arguments.of(...)` even for a single parameter here, since the SPI is typed to `Arguments`. - **Streams are closed** by JUnit after consumption, so `Files.list(...)` is safe — but note `Files.list` is lazy and holds a directory handle until closed, which is exactly why that guarantee matters. - **Empty result**: a provider that yields nothing makes the parameterized test fail by default. For a directory-scanning provider this is a feature — a mis-typed path fails loudly rather than passing with zero cases. - **Exceptions** may be declared (`throws Exception`) and propagate as a failure of the test container. - **Determinism**: if you generate cases, seed the randomness explicitly and report the seed, otherwise a failure is not reproducible. Sort directory listings, since filesystem order is not guaranteed — otherwise invocation indexes shift between machines and failures are hard to correlate. ## Version note on the SPI The long-standing signature is `provideArguments(ExtensionContext)`. Recent JUnit 5 versions (5.13 onwards) added an overload that also receives a `ParameterDeclarations` describing the test method's parameters, which lets a provider adapt to the declared types; the older form remains supported. Check the version in the project before writing the signature from memory. ## Interview summary Say: `@ArgumentsSource` is the base SPI that the built-in sources are themselves built on; use it when the source is reusable logic, needs `ExtensionContext`, or generates/discovers data; make it configurable with a custom annotation plus `AnnotationConsumer`; otherwise prefer `@MethodSource`.
- How does JUnit pass your annotation's attribute values into the provider?The provider implements `AnnotationConsumer<YourAnnotation>`, and JUnit calls `accept(annotationInstance)` after constructing the provider and before calling `provideArguments`. The provider stores what it needs in fields; a fresh provider instance is created per test method, so that is safe. This is the same mechanism JUnit's own @ValueSource and @CsvSource providers use.
- Your provider enumerates files in a directory. What would you do to keep failures reproducible?Sort the listing, because filesystem iteration order is not guaranteed and unsorted order makes invocation indexes differ between machines. Give each invocation a display name derived from the file name rather than the index so a CI failure names the offending file. If cases are randomly generated instead, fix and log the seed so a failing run can be replayed exactly.
- Why does JUnit require the provider to be a top-level or static nested class with a no-arg constructor?JUnit instantiates it reflectively with no arguments and with no enclosing instance available, so an inner (non-static nested) class cannot be constructed and a constructor requiring parameters cannot be called. Configuration therefore arrives through AnnotationConsumer rather than through the constructor.
saying these in an interview costs you the question
- Reaching for a custom provider when a three-row @MethodSource would do
- Trying to configure a provider through its constructor instead of AnnotationConsumer
- Declaring the provider as a non-static inner class
- Assuming provideArguments has no access to test context
- Generating random cases without a fixed, reported seed