When would you register a JUnit 5 Jupiter extension programmatically with @RegisterExtension on a field instead of declaring it with @ExtendWith, and what rules apply to that field?
answer
- @ExtendWith takes a class; @RegisterExtension takes an instance
- Non-private, Extension-typed, non-null at registration
- Builder-configured extensions (WireMock, Testcontainers)
- Test can query the field (port, captured logs)
- @Order for relative ordering of programmatic fields
basics
~20 sUse @RegisterExtension when you must build the extension instance yourself — constructor arguments or a builder — or when the test needs to query it (for example a randomly assigned port). The field must be non-private, of an Extension type, and non-null when JUnit reads it.
solid answer
~50 s`@ExtendWith` names a **class**, so Jupiter constructs it with the no-arg constructor and you cannot configure it. `@RegisterExtension` points at a **field holding an already-built instance**, which unlocks three things: 1. **Configuration** — `WireMockExtension.newInstance().options(wireMockConfig().dynamicPort()).build()`, a fixed `Clock`, a container with specific settings. 2. **Test access** — the field is a normal field, so tests can call it (`wireMock.getPort()`, `logCapture.messages()`). 3. **Explicit ordering** — `@Order` on the field controls relative order among programmatically registered extensions. Rules for the field: it must not be `private`, its type must implement `Extension`, and its value must be non-null when Jupiter evaluates it (otherwise you get an error). It may be `static` or an instance field — and that choice changes *when* the extension is registered and therefore which callbacks it can implement, which is the classic follow-up. Programmatically registered extensions are added after declarative ones in the registry.
code
java · 13 linesclass CheckoutClientTest {
@RegisterExtension
static WireMockExtension stub = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
@Test
void callsPaymentsApi() {
CheckoutClient client = new CheckoutClient("http://localhost:" + stub.getPort());
assertTrue(client.ping());
}
}go deeper
Know it exists and that you use it when the extension needs to be built with arguments, such as a stub server on a random port.
State the field rules (non-private, Extension type, non-null, usually static final) and the two motivations: configuring the instance and letting the test query it.
Discuss ordering with @Order, when static-field initialisation cost matters, and when to expose an extension through a project meta-annotation instead.
Treat it as API design for your test infrastructure: which extensions get a zero-config declarative form, which expose a builder, and how you keep the two paths from drifting.
## The gap @ExtendWith leaves Declarative registration takes a `Class` literal. Jupiter reflectively instantiates it with the default constructor, so there is nowhere to say "listen on this port", "use this fixed clock", "start this image". Many real extensions need exactly that. `@RegisterExtension` closes the gap: instead of a class, you hand Jupiter an **object you constructed**. ```java @RegisterExtension static WireMockExtension wireMock = WireMockExtension.newInstance() .options(wireMockConfig().dynamicPort()) .build(); ``` Jupiter scans the test class for fields annotated `@RegisterExtension`, reads their values, and registers them in the extension registry alongside declaratively registered ones. ## Rules for the field - **Not `private`.** Package-private, protected or public are all fine; `private` is rejected because Jupiter's field discovery deliberately refuses it (the intent is that extensions declared this way are visible to subclasses). - **Type must implement `Extension`.** The compiler will not catch a mistake here, but discovery fails. - **Value must not be `null`** when Jupiter reads it. Assign it inline at the declaration site — assigning it inside `@BeforeEach` is too late, because registration happens before user setup code runs. - **May be `final`.** That is the usual style, and it prevents accidental reassignment mid-run. - **`static` or instance** — the single most important decision, because it determines *when* the extension joins the registry and therefore which callback interfaces are honoured. ## Second benefit: the test can talk to the extension Because it is an ordinary field, the test body can query it. This is how you get a dynamically allocated stub-server port, assert on captured log output, or hand a Testcontainers JDBC URL to a `DynamicPropertySource`. With `@ExtendWith` the instance is hidden inside the engine, so the only way to reach the extension's data is through parameter resolution or the `ExtensionContext` store. ## Ordering Within a context, extensions register in this order: inherited/class-level declarative ones first, then declarative ones in declaration order, then programmatic ones. Among programmatic fields the JVM does **not** guarantee field order, so relative order of two `@RegisterExtension` fields is undefined unless you annotate them with `@Order` (lower value = registered earlier = its "before" callback runs earlier and its "after" callback runs later, since after-callbacks unwind in reverse). If one extension must observe state another produced, make the dependency explicit with `@Order` rather than relying on source order. ## Choosing between the two Use `@ExtendWith` for zero-configuration extensions and for meta-annotations that define your project's test archetypes. Use `@RegisterExtension` when configuration or test access is needed. A common hybrid: a project-specific extension with sensible defaults exposed through `@ExtendWith`, plus a builder-based path via `@RegisterExtension` for tests that need to tune it. ## Interaction with lifecycle One trap: a `static` field is initialised in the static initialiser, so any expensive object it builds is created when the class loads, even if all its tests are filtered out by a condition. If start-up cost matters, have the extension itself start lazily inside a `BeforeAllCallback` rather than in the field initialiser.
- What happens if the @RegisterExtension field is private, or is still null when JUnit reads it?Both fail discovery with a JUnitException rather than silently skipping the extension. Private fields are rejected by design so that subclasses can see inherited extensions; a null value cannot be registered because Jupiter has nothing to add to the registry. The fix is to declare the field non-private and initialise it inline, not in @BeforeEach.
- Two @RegisterExtension fields must run in a specific order. How do you guarantee it?Annotate them with @Order and give explicit values — lower runs earlier. Source order of fields is not a guarantee, because the JVM does not specify the order in which reflection returns declared fields. Remember that after-callbacks unwind in reverse registration order, so the extension registered first wraps the other.
saying these in an interview costs you the question
- Assigning the field in @BeforeEach and expecting the extension to be registered
- Making the field private, which JUnit rejects outright
- Assuming two @RegisterExtension fields run in source order without @Order
- Thinking @RegisterExtension is just a stylistic alternative with no behavioural difference from @ExtendWith