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?
answer
- Tree: engine root → class → @Nested → method
- getParent() / getRoot() walk the chain
- getRequiredTestClass, getDisplayName, getTags, getUniqueId
- getConfigurationParameter = configure a no-arg extension
- getExecutionException in after-callbacks; getStore for state
basics
~20 sExtensionContext 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).
solid answer
~50 s`ExtensionContext` is Jupiter's per-node handle passed into every callback. Nodes form a **tree**: an engine (root) context, a context per test class, a further one per `@Nested` class, and one per test method or test template invocation. `getParent()` walks up one level, `getRoot()` jumps to the engine context. What it offers: - **Identity** — `getElement()`, `getTestClass()`/`getRequiredTestClass()`, `getTestMethod()`, `getTestInstance()`, `getUniqueId()`, `getDisplayName()`, `getTags()`. - **Configuration** — `getConfigurationParameter(key)` reads the same parameters as `junit-platform.properties`, so an extension can be tuned without a constructor. - **Reporting** — `publishReportEntry(...)` attaches key-values that show up in build reports. - **Outcome** — `getExecutionException()` in after-callbacks, which is how `TestWatcher`-style logic sees failures. - **State** — `getStore(Namespace)` returns the scoped key-value store. The hierarchy is what makes scoping work: state put in a method context dies with the method, state in a class context lives for the class, state in the root context lives for the whole run.
code
java · 11 linesclass DiagnosticsExtension implements AfterTestExecutionCallback {
@Override
public void afterTestExecution(ExtensionContext ctx) {
boolean verbose = ctx.getConfigurationParameter("app.test.verbose")
.map(Boolean::parseBoolean).orElse(false);
ctx.getExecutionException().ifPresent(failure ->
ctx.publishReportEntry("failed",
ctx.getRequiredTestClass().getSimpleName() + "#" + ctx.getDisplayName()
+ (verbose ? " :: " + failure : "")));
}
}go deeper
Know that the context tells you which test is running and gives access to the store; naming a couple of accessors is enough.
Describe the engine/class/method tree, getParent/getRoot, and the main accessors including configuration parameters and getExecutionException.
Tie context lifetime to state scoping and thread safety — why fields are wrong, how to reach class- or run-level scope deliberately.
Discuss it as the extension SPI's scoping model, and how it constrains shared-fixture design under parallel execution.
## The execution tree The JUnit Platform models a test run as a tree of descriptors. Jupiter mirrors that tree with `ExtensionContext` objects: ``` engine (root) └── class OrderServiceTest ├── method createsOrder() ├── method rejectsEmptyCart() └── @Nested class WhenCartIsEmpty └── method fails() ``` When a callback fires, the context you receive is the node currently being processed. `BeforeAllCallback.beforeAll` receives the **class** context; `BeforeEachCallback.beforeEach` receives the **method** context whose parent is the class context. A `@Nested` class's context has the outer class context as its parent. `getRoot()` returns the engine-level context, which exists for the entire run. This is why the same extension instance can serve many contexts: an extension is generally a stateless strategy object, and everything *stateful* is keyed by the context it belongs to. ## What you can ask a context **Identity and metadata.** `getTestClass()` and `getTestMethod()` return `Optional`s because not every node has both (a class context has no method). The `getRequired*` variants throw instead of returning empty, which is convenient inside a callback where you know the node type. `getUniqueId()` is the platform's stable identifier for the node — useful as a store key. `getDisplayName()` and `getTags()` let an extension react to naming or `@Tag` metadata, e.g. skipping heavyweight setup for tests tagged `fast`. **Configuration.** `getConfigurationParameter(String)` (and the typed overload with a converter) reads platform configuration parameters — the same values that come from `junit-platform.properties`, system properties, or the launcher request. Since `@ExtendWith`-registered extensions are constructed with a no-arg constructor, this is the idiomatic way to make such an extension configurable. **Reporting.** `publishReportEntry(key, value)` records a timestamped entry that surfaces in the test report — the sanctioned way for an extension to emit information without printing to stdout. **Outcome.** In `AfterTestExecutionCallback`/`AfterEachCallback`, `getExecutionException()` returns the throwable the test failed with, if any. Extensions that dump diagnostics on failure (screenshots, container logs, thread dumps) branch on this. **State.** `getStore(Namespace)` returns a `Store` scoped to *this* context. Because contexts are hierarchical, the store is too: reads consult ancestors, writes are local. That single design decision is what gives you per-method, per-class and per-run state with one API. ## Lifetime rules that matter An `ExtensionContext` is only valid during the callbacks for its node. Do not stash a method context in a field and use it later — it may be closed, and its store cleaned up. If you need state to outlive a node, put it in a **higher** context's store, not in a field on the extension, and definitely not in a static. The practical translation: to make something last for the whole class, ask for it in `beforeAll` (class context) or call `context.getParent()` from a method callback; to make it last for the whole run, use `context.getRoot().getStore(...)`. ## Why not just use fields? Extension instances are not guaranteed to map one-to-one to contexts — one declaratively registered extension serves a class and all its methods, and under parallel execution several methods run concurrently against it. Fields would be shared mutable state with a race condition. The context (and its store) makes the scope explicit and is thread-safe by design.
- Inside beforeEach, how do you reach state that should live for the whole test class?Take the method context you were given and call getParent() to reach the class context, then use its store — or simply do the work in beforeAll where the class context is handed to you directly. Reads from a method-level store also fall back to ancestors, so a value written at class level is visible from the method context without any extra work.
- Why shouldn't an extension keep per-test state in its own fields?Because one extension instance typically serves an entire class and all of its test methods, so a field is shared, not per-test. Under parallel execution multiple tests hit that field concurrently and race. The Store keyed by the current ExtensionContext gives correct scoping and is thread-safe.
Think of the context chain as directories: a method context is a folder inside its class folder inside the run's root folder. You look up through the parents, but you write in your own folder.
saying these in an interview costs you the question
- Assuming one ExtensionContext exists per run rather than one per node
- Caching an ExtensionContext in a field and using it after its node has finished
- Thinking getTestMethod() always returns a value, even in class-level callbacks
- Believing extension instance fields are per-test state