skip to content

What is @ArgumentsSource, and how do you design a custom ArgumentsProvider (and a composed annotation) for reusable parameterized-test data?

level: principalimportance: nice to knowfreq 18%

answer

  1. @ArgumentsSource = custom ArgumentsProvider class
  2. provideArguments(ExtensionContext) → Stream<Arguments>
  3. It's the base all built-in sources extend
  4. Composed annotation: meta-annotate + AnnotationConsumer
  5. Reusable/configurable; seed randomness for reproducibility

basics

~20 s

@ArgumentsSource points a parameterized test at a class you write that implements ArgumentsProvider and produces the argument sets in code. It is the most flexible, reusable source — you can even wrap it in your own custom annotation so tests just say @MyData.

solid answer

~40 s

@ArgumentsSource registers a custom **ArgumentsProvider** — a class whose `provideArguments` method returns a `Stream<Arguments>`, computed however you like. It is the extension point all the built-in sources are built on, and it's the right tool when data generation is non-trivial, stateful in setup, configurable, or shared across many tests. Because providers are classes, they're reusable and testable in isolation. The polish move is a **composed annotation**: meta-annotate your own annotation with `@ArgumentsSource(YourProvider.class)` (and optionally make the provider implement `AnnotationConsumer` to read attributes off that annotation), so call sites read declaratively, e.g. `@RandomStrings(count = 50)`. Compared with @MethodSource, @ArgumentsSource trades the convenience of an inline factory for a first-class, parameterizable, widely-shareable provider — the choice for framework-level or cross-module test data.

code

java · 36 lines
java
import java.lang.annotation.*;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.jupiter.params.support.AnnotationConsumer;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.params.provider.Arguments.arguments;

class CustomSourceDemo {

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @ArgumentsSource(EvenNumbersProvider.class)
    @interface EvenNumbers { int upTo() default 10; }

    static class EvenNumbersProvider
            implements ArgumentsProvider, AnnotationConsumer<EvenNumbers> {
        private int upTo;
        public void accept(EvenNumbers a) { this.upTo = a.upTo(); }
        public Stream<? extends Arguments> provideArguments(ExtensionContext ctx) {
            return IntStream.rangeClosed(0, upTo)
                            .filter(n -> n % 2 == 0)
                            .mapToObj(Arguments::of);
        }
    }

    @ParameterizedTest
    @EvenNumbers(upTo = 8)
    void allEven(int n) {
        assertTrue(n % 2 == 0);
    }
}

go deeper

for a junior

Aware that a custom source class can provide test data, even if they wouldn't write one.

for a middle

Can implement a basic ArgumentsProvider returning Stream<Arguments> and wire it with @ArgumentsSource.

for a senior

Builds a composed annotation with AnnotationConsumer to pass configuration, and chooses @ArgumentsSource vs @MethodSource deliberately for reuse.

for a principal

Designs shared, configurable, reproducible providers (seeded), places them in test-support modules as team conventions, and knows when to graduate to property-based testing instead of bespoke providers.

## Where @ArgumentsSource sits All the convenient sources — `@ValueSource`, `@CsvSource`, `@EnumSource`, `@MethodSource` — are ultimately implemented as **`ArgumentsProvider`s** behind `@ArgumentsSource`. So `@ArgumentsSource` is the **low-level, fully general extension point**: you give it a class that programmatically yields the argument sets. ## The ArgumentsProvider interface ```java public interface ArgumentsProvider { Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception; } ``` (`Arguments` is one row of parameters; `Arguments.of(...)` builds one. `ExtensionContext` gives access to the test class/method, store, tags, etc.) You point a test at it: ```java @ParameterizedTest @ArgumentsSource(FibonacciProvider.class) void t(int n, int fib) { ... } static class FibonacciProvider implements ArgumentsProvider { public Stream<? extends Arguments> provideArguments(ExtensionContext ctx) { return Stream.of(arguments(0,0), arguments(1,1), arguments(7,13)); } } ``` The provider class must be **resolvable** by JUnit: a top-level class, or a `static` nested class, with a no-arg constructor (a `private` no-arg constructor is fine — JUnit makes it accessible). ## When to choose it over @MethodSource Both can compute data, but they differ in **shape and reuse**: - **@MethodSource** — quickest for *local*, one-off data: a factory method next to the test. No new type, but tied to that class (unless fully-qualified) and not configurable beyond what the method hard-codes. - **@ArgumentsSource** — a *reusable, first-class* provider type: ideal when the same generation logic is shared across many tests/modules, when it needs setup/teardown or external resources, or when you want it **parameterizable** via an annotation. It's the choice for framework-level or cross-cutting test data (e.g. "all supported locales", "a fuzz set of malformed inputs"). ## The composed-annotation pattern (the principal-level move) The real power is making the provider read like a built-in source. Define your **own annotation** and meta-annotate it with `@ArgumentsSource`: ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @ArgumentsSource(RandomStringsProvider.class) public @interface RandomStrings { int count() default 10; int maxLen() default 16; } ``` To read `count`/`maxLen`, have the provider implement **`AnnotationConsumer<RandomStrings>`** (it gains an `accept(RandomStrings)` callback invoked before `provideArguments`): ```java class RandomStringsProvider implements ArgumentsProvider, AnnotationConsumer<RandomStrings> { private int count, maxLen; public void accept(RandomStrings a) { this.count = a.count(); this.maxLen = a.maxLen(); } public Stream<? extends Arguments> provideArguments(ExtensionContext ctx) { return IntStream.range(0, count) .mapToObj(i -> arguments(randomString(maxLen))); } } ``` Now tests are crisp and declarative: ```java @ParameterizedTest @RandomStrings(count = 50, maxLen = 8) void handlesArbitraryStrings(String s) { ... } ``` This is exactly how `@CsvSource` etc. are structured internally (annotation + `AnnotationConsumer` + provider). It gives you a vocabulary of domain-specific sources for your codebase. ## Design considerations (principal lens) - **Determinism vs. fuzzing.** Random providers should accept (and log) a **seed** so failures are reproducible; otherwise a red build can't be reliably reproduced. - **Cost.** `provideArguments` runs once per test method; keep generation bounded and avoid hitting real I/O unless intended. - **Statelessness across invocations.** The provider produces the *whole* stream up front; don't smuggle per-invocation mutable state. - **Reuse boundaries.** Put broadly-shared providers in a test-support module so multiple modules consume them; avoid leaking production code into test fixtures and vice versa. - **Readability.** Prefer the composed annotation so call sites don't expose provider plumbing; the annotation name *documents intent* (`@AllSupportedLocales`). - **Know when to stop.** If you're recreating randomized input-space exploration, consider a dedicated **property-based testing** library (jqwik) rather than hand-rolling ever-larger providers. ## Gotchas - Non-static inner provider classes (or missing no-arg constructor) → JUnit can't instantiate them. - Forgetting `RetentionPolicy.RUNTIME` on the composed annotation → it's invisible at runtime. - Implementing `AnnotationConsumer` for the wrong annotation type → attributes never populate.

  • How can a custom annotation pass configuration (like a count) to its ArgumentsProvider?
    The provider implements AnnotationConsumer<TheAnnotation>; JUnit calls accept(theAnnotation) before provideArguments, letting the provider read the annotation's attributes and use them when generating the stream.
  • Why prefer @ArgumentsSource over @MethodSource for cross-module test data?
    @ArgumentsSource is a first-class, reusable, configurable provider type that can live in a shared test-support module and be wrapped in a declarative annotation, whereas @MethodSource ties the data to a factory method (and is awkward to parameterize or share broadly).

saying these in an interview costs you the question

  • Non-static/no-no-arg-constructor provider class JUnit can't instantiate
  • Forgetting RUNTIME retention on the composed annotation
  • Unseeded randomness making failures unreproducible
  • Using @ArgumentsSource for trivial local data where @MethodSource/@CsvSource is simpler

context