skip to content

ExtensionContext & Store

Where an extension keeps state safely across callbacks: the ExtensionContext hierarchy and the namespaced Store. Asked to check you wouldn't just reach for static fields.

on this pageshow

questions

4

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?

level: middleimportance: must knowfreq 30%

answer

  1. Extensions stateless; state in ExtensionContext Store
  2. getStore(Namespace.create(getClass()))
  3. put / get(key, Type.class) / getOrComputeIfAbsent
  4. Read consults ancestors, write is local
  5. Fields = shared across tests = race under parallel execution

basics

~20 s

Use 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 s

Extensions 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 lines
java
class 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

for a junior

Know that state goes in context.getStore(...) with put/get rather than in a field, and that the namespace is usually the extension class.

for a middle

Explain why fields are wrong (one instance serves many tests), the read-up/write-local hierarchy, and getOrComputeIfAbsent for lazy creation.

for a senior

Reason about scope choice per fixture, concurrency guarantees of the store versus the stored object, and cleanup responsibilities.

for a principal

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

context

open as a page

Every JUnit 5 extension callback receives an ExtensionContext object. What does it give you, and what does its parent/root chain represent at run time?

level: middleimportance: should knowfreq 26%

basics

~20 s

ExtensionContext is the handle to the thing currently executing — a test method, a class, or the engine root. It exposes the test class/method, display name, tags, configuration parameters, a report-entry publisher, a key-value Store, and getParent()/getRoot() to walk up the nesting chain (method → class → engine).

open as a page

If a JUnit 5 extension puts something that must be shut down — an open connection pool or a started server — into the ExtensionContext Store, how does it get closed, and when exactly does that happen?

level: seniorimportance: should knowfreq 22%

basics

~20 s

Store the value as an ExtensionContext.Store.CloseableResource (recent JUnit 5 versions also auto-close plain AutoCloseable values). When the context that owns the store is closed — end of the test method, class, or whole run for the root context — JUnit calls close() on those values in reverse insertion order.

open as a page

You need one expensive fixture — say a database container — started once for an entire JUnit 5 test run, reused by many classes, torn down at the end, and safe when tests run in parallel. How do you design that with the extension API, and what tradeoffs are you accepting?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Create it lazily with getOrComputeIfAbsent on the root context's store, in a namespace keyed by your extension, storing a value whose close() shuts it down so the store tears it down at end of run. The tradeoff is speed versus isolation: shared state now needs a per-test reset strategy and a thread-safe fixture.

open as a page