A JUnit 5 extension registered with @RegisterExtension behaves differently depending on whether the field is static or an instance field. What is the difference, and how do you decide which to use?
answer
- static → registered before class-level phase → BeforeAllCallback honoured
- instance → registered after test instance created → per-test callbacks only
- instance field = new extension per test = free isolation
- static = one instance per class = shared state, must reset
- 'beforeAll never fires' → field isn't static
basics
~20 sA static field is registered before class-level setup, so the extension can implement class-level callbacks such as BeforeAllCallback, AfterAllCallback and TestInstancePostProcessor. An instance field is registered only after the test instance is created, so class-level callbacks are not honoured — only per-test ones like BeforeEachCallback.
solid answer
~50 sThe field's `static`-ness decides **when** the extension enters the registry, and that decides which callbacks can possibly fire. - **Static field** — registered before any class-level lifecycle work. The extension can implement `BeforeAllCallback`, `AfterAllCallback`, `TestInstancePostProcessor`/`TestInstanceFactory`, and every method-level callback. Its state naturally lives for the whole class, so it is what you use for an expensive shared resource: a container, a stub server, an embedded broker. - **Instance field** — registered after the test class has been instantiated and post-processed, i.e. once per test with the default per-method lifecycle. Class-level callbacks are **not honoured** (silently, in older versions), so a `BeforeAllCallback` on an instance field simply never runs. It gives you a fresh extension instance per test, which is exactly right for per-test state such as a captured-log buffer or a temp directory. Rule of thumb: shared/expensive and needing `@BeforeAll`-time work → `static`; per-test isolation → instance. If a class-level callback mysteriously never fires, check that the field is static — that is the classic diagnosis.
code
java · 14 linesclass OrderRepositoryTest {
@RegisterExtension
static PostgresExtension db = new PostgresExtension("postgres:16"); // BeforeAllCallback honoured
@RegisterExtension
LogCaptureExtension logs = new LogCaptureExtension(); // fresh per test
@Test
void logsOnInsert() {
new OrderRepository(db.dataSource()).insert(anOrder());
assertTrue(logs.messages().stream().anyMatch(m -> m.contains("inserted")));
}
}go deeper
Remember the rule: static field for setup that must run once for the class, instance field for something fresh per test.
Explain registration timing as the cause — static is registered before the class-level phase, instance only after the test object exists — and the isolation consequence.
Use it diagnostically ('beforeAll never fires' vs 'state leaks between tests') and design the hybrid: expensive resource static, per-test reset separate.
Frame it as lifecycle cost versus isolation policy across the suite — how much shared state the team tolerates for speed, and how that interacts with parallel execution.
## Registration time is the whole story Jupiter's execution of a test class runs roughly: build the extension registry for the class → run `BeforeAllCallback`s and `@BeforeAll` methods → for each test, create the test instance, run `TestInstancePostProcessor`s, then `BeforeEachCallback`s and `@BeforeEach`, execute, then the "after" callbacks in reverse → finally `AfterAllCallback`s. A **static** `@RegisterExtension` field can be read without an instance, so Jupiter registers it while building the class-level registry — before the class-level phase begins. Therefore all callback interfaces it implements are live, including `BeforeAllCallback` and `AfterAllCallback`. An **instance** field cannot be read until an object exists. Jupiter registers it after constructing the test instance and after `TestInstancePostProcessor`s have run. By then the class-level phase is over, so any `BeforeAllCallback`/`AfterAllCallback`/`TestInstancePostProcessor` the extension implements is simply not invoked. The extension still gets `BeforeEachCallback`, `AfterEachCallback`, `BeforeTestExecutionCallback`, `AfterTestExecutionCallback`, `ParameterResolver` for method parameters, `TestExecutionExceptionHandler`, and so on. ## Consequence for state With the default `@TestInstance(Lifecycle.PER_METHOD)`, a new test instance is created per test method, so an instance-field extension is a **new object per test**. Any state it holds is automatically isolated between tests — no cleanup required. A static-field extension is a **single object for the whole class** (and any state it accumulates leaks between tests unless it clears it in an after-each callback). That difference is often more important than the callback question: it decides whether you get isolation for free or must engineer it. With `@TestInstance(Lifecycle.PER_CLASS)` the test instance is created once, so an instance field is created once too. Do not rely on that to resurrect class-level callbacks, though: if you need `BeforeAllCallback`, be explicit and make the field `static`. Depending on subtle lifecycle interactions to enable a callback is exactly the kind of thing that breaks on a JUnit upgrade or when someone changes the lifecycle annotation. ## Choosing Use **static** when: - Startup is expensive and should happen once per class — Testcontainers, an embedded Kafka/Postgres, a WireMock server. - The extension must do work in the `@BeforeAll` window, e.g. publish system properties or a JDBC URL other class-level setup depends on. Use an **instance field** when: - The extension holds per-test state you want reset automatically — captured logs, a per-test temp folder, a recording of events. - Tests may run in parallel at method level and sharing would create races. A very common hybrid: a static extension owning the expensive resource plus an instance extension (or an after-each callback on the static one) that resets data between tests — start the database once, truncate or roll back per test. ## Diagnosing Symptom: "my extension's `beforeAll` never runs." Ninety percent of the time the field is not static. Symptom: "state from test A leaks into test B." Ninety percent of the time the field *is* static and the extension keeps mutable state without resetting it. Both diagnoses come straight from registration timing.
- An extension registered on an instance field implements BeforeAllCallback. What happens at run time?Nothing — the callback is simply never invoked, because by the time an instance field can be read the class-level phase has already completed. There is usually no loud error, which is what makes it a nasty bug. The fix is to make the field static so the extension is registered while the class-level registry is being built.
- How does @TestInstance(Lifecycle.PER_CLASS) change the picture?It changes instance creation, not registration semantics you should depend on: the test class is instantiated once, so an instance-field extension is also created once and its state is shared across the class's tests. That removes the free per-test isolation you normally get. If you need class-level callbacks, still use a static field rather than relying on the lifecycle setting.
saying these in an interview costs you the question
- Believing static vs instance is only about memory or style, with no behavioural difference
- Expecting beforeAll/afterAll to fire from an instance-field extension
- Assuming a static extension resets its state between tests
- Claiming JUnit throws a clear error when a class-level callback cannot be honoured