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?
answer
- getRoot().getStore(ns).getOrComputeIfAbsent(...) = once per run
- Closeable value → torn down at end of run, LIFO
- Key by configuration, not one global singleton
- Shared fixture ⇒ reset policy + @ResourceLock
- Best-effort cleanup: external reaper for hard kills
basics
~20 sCreate 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.
solid answer
~50 s**Mechanism.** In `beforeAll` (or a `ParameterResolver`), do: ```java ctx.getRoot().getStore(Namespace.create(DbExtension.class)) .getOrComputeIfAbsent("db", k -> new SharedDb(), SharedDb.class); ``` Root store → lives for the run; `getOrComputeIfAbsent` → created exactly once even if several classes race; closeable value → shut down when the run ends, in reverse insertion order. **Tradeoffs I am accepting.** - *Isolation for speed.* One database for the whole suite means tests share data. I need an explicit reset policy — transaction rollback per test, truncation, or per-test schemas/prefixed keys — and that policy is now part of the fixture's contract. - *Thread safety.* The store is concurrent; the fixture is not. Parallel tests hitting one connection pool or one stub server need either a thread-safe design or per-thread partitioning. - *Failure blast radius.* If it dies, every test fails, and diagnosis is harder than with per-class fixtures. - *Cleanup is best-effort.* A killed JVM closes nothing, so infrastructure needs its own reaper. I would reserve run-wide scope for genuinely expensive fixtures and default to per-class otherwise.
code
java · 17 linesclass DbExtension implements ParameterResolver {
private static final Namespace NS = Namespace.create(DbExtension.class);
@Override
public boolean supportsParameter(ParameterContext pc, ExtensionContext ctx) {
return pc.getParameter().getType() == DataSource.class;
}
@Override
public Object resolveParameter(ParameterContext pc, ExtensionContext ctx) {
String image = ctx.getConfigurationParameter("app.test.db.image").orElse("postgres:16");
return ctx.getRoot().getStore(NS)
.getOrComputeIfAbsent(image, SharedDb::new, SharedDb.class)
.dataSource();
}
}go deeper
Recognise the pattern: get the store from the root context and use getOrComputeIfAbsent so the fixture starts once.
Explain lazy single creation, root scope for run lifetime, and closeable teardown, plus why data must be reset between tests.
Argue the scope choice with evidence, describe the reset strategy, the parallelism policy, and failure diagnosis for a shared fixture.
Own it as suite-wide policy: default per-class, promote to run-wide only on measured startup cost, key by configuration, enforce isolation in the extension, and plan for best-effort cleanup with an external reaper.
## The mechanism, precisely Three store properties combine into the pattern: 1. **Scope = context.** `getRoot()` returns the engine-level context, alive for the entire launcher execution, so anything in its store outlives individual classes. 2. **Single creation.** `getOrComputeIfAbsent(key, creator, type)` returns the existing value or computes, stores and returns it, and is built for concurrent access — so with parallel classes only one container starts. 3. **Automatic teardown.** A stored value implementing the store's closeable contract (`ExtensionContext.Store.CloseableResource`; on JUnit 5.13+ plain `AutoCloseable` too) is closed when the root context closes, i.e. at the end of the run, in reverse insertion order. Hand the fixture to tests through a `ParameterResolver` (inject a `DataSource` or a JDBC URL) rather than a static accessor, so the dependency is visible in the test signature and nothing depends on classloading order. ## The real decision: what scope, not how The API makes all three scopes equally easy, so the engineering question is which to pick. - **Per test (method store).** Maximum isolation, zero cross-talk, no reset logic. Only viable when creation is cheap. - **Per class (class store).** The usual sweet spot: start once per class, and classes that need different configurations get different instances. Failure blast radius stays small and parallel *class* execution keeps instances separate. - **Per run (root store).** Only for fixtures whose startup dominates the suite — containers, embedded brokers, a heavy Spring context. Everything else is a consequence you now own. A useful refinement is **keying by configuration**: use `getOrComputeIfAbsent(configKey, ...)` where the key encodes image, version and settings. Classes with identical needs share one instance; classes that genuinely differ get their own. That is how Spring's test-context caching behaves, and it beats a single global singleton because it degrades gracefully. ## Consequences you sign up for **State bleed.** Shared fixture = shared data. You must choose a reset strategy and enforce it: a transaction rolled back per test (fast, but hides commit-time behaviour), truncation between tests (slower, more realistic), or namespacing — a schema, database or key prefix per class. Whichever you choose, it belongs in the extension so no test can forget it. **Concurrency.** The store is thread-safe; the thing inside is not. Under method-level parallelism, tests share one connection pool, one stub server, one message broker. Either make the fixture safe to use concurrently and give each test its own logical partition, or restrict parallelism (Jupiter's `@ResourceLock` and `@Execution` annotations exist precisely for this) — and accept the throughput cost. **Diagnosability.** Per-class fixtures fail locally: one class goes red. A run-wide fixture that misbehaves fails everything, and the failure often appears in whichever test happened to run when the state went bad. Budget for good failure output: dump container logs on failure via `getExecutionException()`, publish the fixture's identity as a report entry. **Startup on the critical path.** Lazy creation means the first test that needs it pays. That is usually right — a filtered run of one unrelated class should not start a database — but it makes that one test look slow and can trip timeouts. Consider an explicit warm-up when the run is known to need it. **Cleanup is best-effort.** `close()` requires the run to end normally. CI kills, OOM and `System.exit` skip it, so anything that costs money or holds ports needs an external reaper (Testcontainers' Ryuk, a CI cleanup step). Also remember that with a JVM-reusing or forked build, "root" means per launcher execution, not per machine — reuse across forks needs an out-of-process mechanism, not the store. ## How I would state the decision Default to per-class. Promote to run-wide only when measurement shows fixture startup dominates the suite, and only together with an enforced reset strategy, a parallelism policy, and an external reaper. Write it down as a suite-level convention, because the choice is invisible in any individual test yet governs all of them.
- With a run-wide shared database, how do you keep tests isolated from each other's data?Pick one enforced strategy rather than trusting authors: wrap each test in a transaction that is rolled back, truncate the affected tables in an after-each callback, or give each class its own schema or key prefix. Rollback is fastest but hides commit-time behaviour such as triggers and constraint timing; truncation is slower but realistic. Whichever you choose, implement it inside the extension so no test can skip it.
- Method-level parallel execution is enabled and several tests share this one fixture. What do you do?The store's thread safety only guarantees a single instance, not safe concurrent use of it. Either make the fixture genuinely concurrent-safe and partition per test (separate schemas, unique key prefixes, distinct stub mappings), or serialise access with Jupiter's @ResourceLock on the shared resource and accept the throughput loss. Measure both — sometimes per-class fixtures with parallel classes beat one shared fixture with locking.
- Why prefer the root store over a static field holding the container plus a JVM shutdown hook?The store gives framework-controlled lifetime and ordering: creation is atomic through getOrComputeIfAbsent, teardown happens at a defined point in the run in reverse insertion order, and nothing depends on classloading or JVM exit semantics. Shutdown hooks run in unspecified order, may be skipped, and a static field is invisible to the engine, so it cannot participate in per-scope cleanup or be keyed by configuration.
saying these in an interview costs you the question
- Assuming the store's thread safety makes the shared fixture itself thread-safe
- Sharing one fixture run-wide with no explicit per-test reset strategy
- Relying on close() to run when CI kills the job
- Treating 'root context' as process-wide when the build forks or reuses JVMs
- Jumping straight to run-wide scope without measuring that startup actually dominates