How does an extension in JUnit 5 keep state between its callbacks — for example a start timestamp recorded before a test and read after it — and why are instance fields on the extension the wrong place?
answer
- Extensions stateless; state in ExtensionContext Store
- getStore(Namespace.create(getClass()))
- put / get(key, Type.class) / getOrComputeIfAbsent
- Read consults ancestors, write is local
- Fields = shared across tests = race under parallel execution
basics
~20 sUse the Store obtained from the ExtensionContext: context.getStore(Namespace.create(MyExtension.class)).put(key, value) and get(key, Type.class), or getOrComputeIfAbsent(key, creator, Type.class). The store is scoped to the current context and thread-safe; extension fields are shared across all tests the extension serves and race under parallel execution.
solid answer
~50 sExtensions must be treated as **stateless strategies**; state lives in the `ExtensionContext`'s `Store`. ```java Store store = context.getStore(Namespace.create(getClass(), context.getRequiredTestMethod())); store.put("start", System.nanoTime()); long start = store.get("start", long.class); ``` `getOrComputeIfAbsent(key, creator, Type.class)` is the workhorse: return the existing value or create-and-store it atomically, which is how you lazily build an expensive resource exactly once per scope. Why not fields? One declaratively registered extension instance typically serves a whole class and every method in it, so a field is *not* per-test state — one test overwrites another's value. With parallel execution that becomes a genuine data race. The store, in contrast, is keyed by context: put it in the method context and it is per-test; put it in the class or root context and it is shared for that scope. It is backed by a concurrent map, so it is safe under parallel execution. Lookups also consult ancestor stores, while writes always land in the local one.
code
java · 16 linesclass TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private static final Namespace NS = Namespace.create(TimingExtension.class);
@Override
public void beforeTestExecution(ExtensionContext ctx) {
ctx.getStore(NS).put("start", System.nanoTime());
}
@Override
public void afterTestExecution(ExtensionContext ctx) {
long start = ctx.getStore(NS).get("start", long.class);
ctx.publishReportEntry("durationMs",
String.valueOf((System.nanoTime() - start) / 1_000_000));
}
}go deeper
Know that state goes in context.getStore(...) with put/get rather than in a field, and that the namespace is usually the extension class.
Explain why fields are wrong (one instance serves many tests), the read-up/write-local hierarchy, and getOrComputeIfAbsent for lazy creation.
Reason about scope choice per fixture, concurrency guarantees of the store versus the stored object, and cleanup responsibilities.
Position it as the fixture-scoping contract for the suite: which state is per-test, per-class or per-run, and what that implies for parallelism and test isolation policy.
## The problem A timing extension needs a start time in `beforeTestExecution` and reads it in `afterTestExecution`. The obvious move is a field. It is wrong. Jupiter creates *few* extension objects: with `@ExtendWith` on a class, one instance serves that class and all its test methods; with auto-detection, one instance may serve the entire run. A field is therefore shared state across many tests. Sequentially, one test simply clobbers another's value. Under `junit.jupiter.execution.parallel.enabled=true`, two tests write and read the field concurrently — a real race producing nonsense timings or worse. ## The Store Every `ExtensionContext` exposes `getStore(Namespace)`, returning a `Store`: a namespaced, hierarchical, thread-safe key-value map whose lifetime is tied to the context. Core operations: - `put(Object key, Object value)` — writes into **this** context's store. - `get(Object key)` / `get(Object key, Class<V> type)` — the typed variant casts for you and throws a clear error on mismatch. - `getOrComputeIfAbsent(K key, Function<K,V> creator)` and the typed overload — return the existing value or compute, store and return it. - `remove(key)` / `remove(key, type)` — take a value out. `getOrComputeIfAbsent` is the idiom for lazily creating something exactly once per scope: a database container, a Spring context, a browser session. It is designed for concurrent access, so parallel tests sharing a class-level store do not each build their own copy. ## Hierarchy: read up, write local Stores mirror the context tree. A **read** on a method-level store that misses falls back to the parent (class), then to the root (engine). A **write** always goes to the store you called it on. Two consequences: 1. Scope is chosen by *which context* you call `getStore` on. `context.getStore(ns)` in a method callback → per-test. `context.getParent().get().getStore(ns)` or the context handed to `beforeAll` → per-class. `context.getRoot().getStore(ns)` → per-run. 2. Values computed in a method context are **not** visible to sibling methods, which is exactly the isolation you want by default. Note that a value found in an ancestor is returned but not copied down; and `getOrComputeIfAbsent` called on a child store when an ancestor already holds the key returns the ancestor's value rather than computing a second one. ## Namespaces `getStore` requires a `Namespace`, created with `Namespace.create(Object... parts)`. Keys are only unique *within* a namespace, so two extensions can both use the key `"start"` without colliding — provided each namespaces by something unique, conventionally its own class: `Namespace.create(MyExtension.class)`. There is also `Namespace.GLOBAL`, which is exactly the shared bucket you should avoid unless you deliberately want cross-extension sharing. Composing extra parts into the namespace (e.g. the test method, or a qualifier from an annotation) is a clean way to get several independent slots from one extension. ## Cleanup Store contents are discarded when the owning context closes. If the value holds an OS resource, implement the store's closeable-resource contract so it is shut down at that moment rather than leaked — the store will close such values automatically in reverse insertion order. ## Rule of thumb No mutable fields in extensions. State goes in the store, scoped by the context you choose, namespaced by your extension class, and cleaned up by the store rather than by hand.
- You call getOrComputeIfAbsent on a method-level store, but the same key already exists in the class-level store. What happens?The lookup walks up the hierarchy, finds the ancestor's value and returns it without invoking the creator function, so nothing is computed twice and nothing is written to the method store. Writes are always local, but reads — including the read inside getOrComputeIfAbsent — consult ancestors. That is how a class-level fixture is transparently reused by every test.
- How do you make store state live for the whole test run rather than one test?Ask for the store on a higher context: context.getRoot().getStore(namespace). The root context is the engine-level node that exists for the entire run, so values there survive across classes and are cleaned up only when the run ends. Combine it with getOrComputeIfAbsent so the first test that needs the resource creates it and everyone else reuses it.
- Is the Store safe when tests run in parallel?Yes — the store is backed by a concurrent map and getOrComputeIfAbsent is designed for concurrent use, so a single value is created even if several threads race for the same key. What the store cannot do is make the stored object itself thread-safe: if you share one mutable fixture across parallel tests you must handle its concurrency, or scope it per test instead.
The store is a coat check attached to each scope: you hand over an item and get it back with a ticket, and when the scope closes everything left is collected. A field on the extension is a coat hook in the lobby that everyone shares.
saying these in an interview costs you the question
- Keeping per-test state in extension instance fields or statics
- Assuming a new extension instance is created per test method
- Thinking writes propagate to parent stores (they do not — only reads walk up)
- Using Namespace.GLOBAL for everything and risking key collisions between extensions
- Believing the store makes the objects it holds thread-safe