How do you make JUnit 5 hand your test methods a ready-made collaborator — say a pre-configured HTTP client — as a method argument? Walk through the extension interface, its two methods, and how you keep it from claiming parameters it should not.
answer
- ParameterResolver: supportsParameter + resolveParameter
- ParameterContext: getParameter/getIndex/isAnnotated/getTarget
- claim by marker annotation AND type — never a bare common type
- resolveParameter must return an assignable instance
- resolution runs per invocation; constructors and lifecycle methods included
basics
~20 sImplement ParameterResolver: supportsParameter(ParameterContext, ExtensionContext) returns true only for parameters you own, and resolveParameter(...) returns the object. Register it with @ExtendWith. Narrow supportsParameter by both a custom annotation and the parameter type so it never competes with another resolver.
solid answer
~50 sI implement `ParameterResolver`: ```java boolean supportsParameter(ParameterContext pc, ExtensionContext ec); Object resolveParameter(ParameterContext pc, ExtensionContext ec); ``` `supportsParameter` is the claim: JUnit asks every registered resolver about every declared parameter, so mine must be narrow — I check the parameter type **and** a marker annotation, using `ParameterContext.isAnnotated(ConfiguredClient.class)` and `pc.getParameter().getType()`. `resolveParameter` is only called when I returned `true`, and it must return an instance assignable to the declared type; anything else becomes a `ParameterResolutionException`. The resolver is registered like any extension — `@ExtendWith(HttpClientResolver.class)`, usually hidden behind a composed annotation. It applies to test constructors, `@BeforeEach`/`@AfterEach`, `@BeforeAll`/`@AfterAll` and test methods, so if the object is only valid per test I guard on `pc.getDeclaringExecutable()` or on context. Resolution runs per invocation, so each test gets its own instance unless I cache one deliberately. Two resolvers claiming the same parameter is an error, not a race.
code
java · 20 lines@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ConfiguredClient { }
public class HttpClientResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) {
return pc.isAnnotated(ConfiguredClient.class)
&& HttpClient.class.isAssignableFrom(pc.getParameter().getType());
}
@Override
public Object resolveParameter(ParameterContext pc, ExtensionContext ec) {
return HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.followRedirects(HttpClient.Redirect.NEVER)
.build();
}
}go deeper
Name the interface and its two methods, and show a resolver claiming an annotated parameter and returning the object.
Explain the two-phase protocol, ParameterContext accessors, narrowing the claim by annotation plus type, and registration via @ExtendWith.
Cover lifetime and caching, cleanup of injected resources, purity of supportsParameter, and the reach across constructors and lifecycle methods.
Weigh a hand-rolled resolver against an existing DI extension or plain factory helpers, and set conventions so a set of resolvers does not quietly become an undocumented container.
## The interface ```java public interface ParameterResolver extends Extension { boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException; Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException; } ``` The protocol is two-phase and deliberately simple. For **each parameter** of an executable the engine is about to invoke, it asks every registered resolver `supportsParameter`. Exactly one must say yes. Then it calls `resolveParameter` on that one and passes the returned object as the argument. ## ParameterContext — what you decide on `ParameterContext` describes the single parameter under consideration: - `getParameter()` — the reflective `java.lang.reflect.Parameter` (type, name if compiled with `-parameters`, annotations); - `getIndex()` — its zero-based position; - `getDeclaringExecutable()` — the constructor or method it belongs to; - `getTarget()` — `Optional<Object>`, the test instance; empty for a constructor or a static `@BeforeAll`; - `isAnnotated(Class)`, `findAnnotation(Class)`, `findRepeatableAnnotations(Class)` — annotation lookups on the parameter. `ExtensionContext` gives the surrounding node: test class, test method, display name, tags, configuration parameters, and the store for keeping state. ## Writing supportsParameter well This method is the whole design problem. A resolver that claims too much collides with other resolvers and produces run-wide failures; one that claims too little silently fails to inject. **Claim by annotation plus type.** Type alone is risky: `String` or `Path` parameters are claimed by other extensions, and a broad claim on a common type will eventually collide. A marker annotation makes the claim explicit at the call site and reads well: ```java public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) { return pc.isAnnotated(ConfiguredClient.class) && HttpClient.class.isAssignableFrom(pc.getParameter().getType()); } ``` **Be side-effect free.** `supportsParameter` may be called for parameters you do not own and for executables you never resolve; do not open sockets or mutate state there. Do the work in `resolveParameter`. **Do not throw to mean 'no'.** Return `false`. An exception from `supportsParameter` is wrapped in a `ParameterResolutionException` and fails the test. ## Writing resolveParameter well It is invoked only after your `supportsParameter` returned `true`, so it can assume the parameter is yours. It must return an object **assignable to the declared parameter type**; returning the wrong type — or `null` for a primitive parameter — is reported as a `ParameterResolutionException`. Returning `null` for a reference type is allowed but usually a smell. Lifetime is the second design decision. Resolution happens **per invocation**: a new call for every test method, every repetition, every parameterized argument set, and separately for the constructor and each lifecycle method. So a naive `new HttpClient()` in `resolveParameter` creates one per injection point — perfectly isolated, possibly expensive. When you want one shared instance per test or per class, build it once and keep it in the extension context store under the appropriate context (test-level or root-level), which also gives you deterministic cleanup when the store is closed. ## Registration and reach Register it as any extension: `@ExtendWith(HttpClientResolver.class)` on the class or method, a `@RegisterExtension` field (useful when the resolver needs constructor arguments), automatic `ServiceLoader` registration, or — most readably — a composed annotation that carries both the marker and the registration. A registered resolver participates in **all** injection points of the nodes it covers: the test class constructor, `@BeforeAll`/`@AfterAll`, `@BeforeEach`/`@AfterEach`, `@Test`, `@TestFactory` and `@TestTemplate` methods. That reach is why narrow claims matter: your resolver is asked about the parameters of every one of those. ## A complete example ```java @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface ConfiguredClient { } public class HttpClientResolver implements ParameterResolver { @Override public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) { return pc.isAnnotated(ConfiguredClient.class) && pc.getParameter().getType() == HttpClient.class; } @Override public Object resolveParameter(ParameterContext pc, ExtensionContext ec) { return HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(2)) .build(); } } ``` Used as `@Test void fetches(@ConfiguredClient HttpClient client) { ... }`. ## Why this rather than a field and a setup method A `@BeforeEach` that assigns a field works and is fine for a single class. A resolver earns its place when the same collaborator is needed across many test classes, when its construction is non-trivial or needs cleanup, or when it should differ per test based on annotations at the injection point. It also keeps test classes free of setup boilerplate and makes the dependency visible in the method signature — the test reads as a function of its inputs. The costs are real too: an extension is indirection, and a resolver that injects half a dozen types becomes a home-grown DI container that nobody documented. Keep each resolver responsible for one clearly named thing.
- Your resolver returns `false` from supportsParameter for a parameter you meant to own. What does the run look like?If no other resolver claims it, the test fails before its body runs with a `ParameterResolutionException` stating that no `ParameterResolver` is registered for that parameter in that executable. The usual causes are a missing `@ExtendWith`, a marker annotation without `RUNTIME` retention, or a type check that is too strict (`==` where the declared type is a supertype).
- How would you give every test in a class the same instance instead of a fresh one per method?Build it once and cache it, keyed in the extension context store at the class-level (or root) context rather than constructing inside `resolveParameter` each time; the resolver then looks it up and creates it only on first use. Storing a closeable resource there also gets it closed deterministically when that context is closed.
- Should supportsParameter ever have side effects?No. It is a pure predicate that may be called for parameters your resolver does not own, on executables that are never invoked, and potentially more than once. Any construction, connection or mutation belongs in `resolveParameter`, which only runs after you have claimed the parameter.
saying these in an interview costs you the question
- Claiming a broad common type such as `String` or `Object` in supportsParameter, guaranteeing collisions
- Throwing an exception from supportsParameter to signal 'not mine' instead of returning false
- Doing expensive work or opening resources inside supportsParameter
- Assuming the resolver only applies to `@Test` methods, forgetting constructors and lifecycle methods
- Expecting one instance per class by default — resolution runs per invocation