skip to content

Extension Model

Jupiter's extension model — callbacks, parameter resolvers, @ExtendWith and @RegisterExtension — and how Mockito and Spring plug into it. Asked because understanding extensions explains what @SpringBootTest is actually doing under the hood.

on this pageshow

explore

questions

28

In JUnit 5 (Jupiter), how do you plug a reusable extension such as the Mockito or Spring test integration into a test, and which declaration sites are valid for the @ExtendWith annotation?

level: juniorimportance: must knowfreq 60%

answer

  1. @ExtendWith = declarative, class/method/field/parameter/meta-annotation
  2. No-arg constructor → cannot configure
  3. Inherited by subclasses and @Nested
  4. Repeatable + array; duplicates deduped
  5. Stackable, unlike JUnit 4 @RunWith

basics

~20 s

Declaratively, with @ExtendWith(SomeExtension.class). It can go on a test class, on a single test method, on a field or parameter, or on your own meta-annotation. JUnit builds the extension with its no-arg constructor, and class-level registration is inherited by subclasses and @Nested classes.

solid answer

~50 s

JUnit 5 replaced JUnit 4's runners and rules with one **Extension** API, and the everyday way to register one is declarative `@ExtendWith`. - On a **class**: `@ExtendWith(MockitoExtension.class)` applies to every test in it, and is inherited by subclasses and `@Nested` classes. - On a **test method**: applies to that method only. - On a **field or parameter** (Jupiter 5.8+): useful for a `ParameterResolver` you want for one argument. - It is repeatable and takes an array: `@ExtendWith({A.class, B.class})`. - It composes: put `@ExtendWith` on your own annotation and you have a `@SpringBootTest`-style meta-annotation. Unlike JUnit 4's `@RunWith`, you can stack as many extensions as you want. The limitation is that Jupiter instantiates the extension through its **no-arg constructor**, so you cannot configure the instance. When the extension needs constructor arguments or a builder, switch to programmatic registration with `@RegisterExtension` on a field. Declaring the same extension type twice in one context is harmless — Jupiter registers each implementation once.

code

java · 15 lines
java
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Test
    @ExtendWith(TimingExtension.class)
    void slowPathIsMeasured() { }
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith({MockitoExtension.class, TimingExtension.class})
@interface UnitTest { }

@UnitTest
class PaymentServiceTest { }

go deeper

for a junior

Know that @ExtendWith(X.class) on the class is how you turn on Mockito or Spring in a JUnit 5 test, and that you can list several.

for a middle

Add the other targets (method, field/parameter, meta-annotation), inheritance to subclasses and @Nested, and the no-arg-constructor limitation that pushes you to @RegisterExtension.

for a senior

Explain the registry model — deduplication, registration order, before/after callbacks nesting like try/finally — and design shared meta-annotations for a codebase's test archetypes.

for a principal

Frame it as the team's testing contract: a small set of curated composed annotations (@UnitTest, @IntegrationTest) so test setup is uniform and swappable, rather than every class hand-rolling extension lists.

## One plug-in mechanism instead of two JUnit 4 had two extension points that did not compose. `@RunWith(SomeRunner.class)` was powerful but exclusive: one runner per class, so you could not have the Spring runner and the Mockito runner at the same time. `@Rule`/`@ClassRule` composed but could only wrap statement execution. Jupiter merges both ideas into the `org.junit.jupiter.api.extension.Extension` marker interface, which sub-interfaces such as `BeforeEachCallback`, `ParameterResolver`, `TestInstancePostProcessor` and `ExecutionCondition` extend. An extension is just a class implementing one or more of those callback interfaces; registering it means telling Jupiter to add it to the extension registry for some scope. ## Declarative registration with @ExtendWith `@ExtendWith` is the declarative path — you name a **class**, Jupiter creates the instance for you. Valid targets: 1. **Test class** — the extension participates in every test in that class. Because Jupiter honours annotation inheritance, an abstract `IntegrationTestBase` annotated with `@ExtendWith` passes it to every subclass, and `@Nested` inner classes inherit from the enclosing class. 2. **Test method** — scoped to that one method; class-level callbacks such as `BeforeAllCallback` cannot fire meaningfully there because the class-level phase has already happened. 3. **Field or parameter** (since Jupiter 5.8) — mostly for `ParameterResolver`s that should only apply to one injection point. 4. **Another annotation** (meta-annotation) — this is how `@SpringBootTest` and `@DataJpaTest` work: they are ordinary annotations that themselves carry `@ExtendWith(SpringExtension.class)`, so the user never types `@ExtendWith`. `@ExtendWith` is `@Repeatable` and its value is an array, so `@ExtendWith({A.class, B.class})` and two stacked annotations are equivalent. ## Instantiation and deduplication Jupiter instantiates a declaratively registered extension via its **default (no-arg) constructor**. That is the whole reason `@RegisterExtension` exists: with `@ExtendWith` there is no place to pass a port number, a builder result, or a preconfigured client. The extension class must be concrete, and it must not be `private`. Jupiter also **deduplicates**: the same extension implementation registered more than once inside one registry hierarchy is only applied once. So a base class with `@ExtendWith(SpringExtension.class)` plus a subclass repeating it does not double the callbacks. This is why meta-annotations can be layered freely. ## Ordering, briefly Extensions are registered in a deterministic order: inherited/class-level declarations first (outermost class down), then the order in which `@ExtendWith` annotations appear, and programmatically registered extensions after them. "Before" callbacks then run in registration order and "after" callbacks in reverse — an onion, like nested try/finally blocks. Relying on the exact interleaving of unrelated extensions is fragile; if order matters, use `@Order` on programmatically registered fields. ## Choosing between the two styles Use `@ExtendWith` when the extension needs no configuration — Mockito, Spring, your own logging or clock-freezing extension with sensible defaults. Use `@RegisterExtension` when you must build the instance, or when the test body needs to talk to it (for example asking a WireMock extension for its randomly assigned port). Both end up in the same registry; the annotation only decides who constructs the object.

  • Why can you stack several extensions in JUnit 5 when JUnit 4 allowed only one @RunWith?
    A JUnit 4 runner owned the whole execution lifecycle of the class, so two runners would each want to drive the run and could not be combined. Jupiter inverts that: the engine drives execution and extensions only implement narrow callback interfaces the engine invokes at defined points. Because callbacks compose (they nest like try/finally), any number can be registered.
  • What happens if the same extension class is registered on both a base class and its subclass?
    Nothing bad — Jupiter deduplicates extension implementations within a registry hierarchy, so it is applied once. This is what makes layered meta-annotations safe: @SpringBootTest on a subclass of a base class that already pulls in SpringExtension does not run Spring's callbacks twice.
  • How would you write a reusable annotation that pulls in several extensions plus a tag?
    Create your own annotation with @Retention(RUNTIME) and the right @Target, then annotate it with @ExtendWith({...}) and @Tag("integration"). Jupiter resolves meta-annotations recursively, so applying your annotation to a test class registers everything it carries. This is exactly the mechanism behind @SpringBootTest and @DataJpaTest.

@ExtendWith is like listing plugin class names in a config file: the framework news up each one for you with defaults. @RegisterExtension is handing the framework an object you built yourself.

saying these in an interview costs you the question

  • Claiming you can only have one extension per class, confusing it with JUnit 4's @RunWith
  • Thinking @ExtendWith lets you pass constructor arguments or configuration to the extension
  • Believing extensions are not inherited, so every subclass must repeat the annotation
  • Saying JUnit 5 still supports @Rule natively (it does not; only the junit-jupiter-migrationsupport module offers limited rule support)

context

open as a page

In JUnit 5, how do you obtain a throwaway directory for a test that needs to read and write real files, and what field or parameter types can receive it?

level: juniorimportance: must knowfreq 55%

basics

~20 s

Annotate a field or a method parameter of type java.nio.file.Path or java.io.File with JUnit Jupiter's @TempDir. Jupiter's built-in extension creates a fresh empty directory in the OS temp location before use and recursively deletes it, with its contents, afterwards.

open as a page

JUnit 5's extension model defines BeforeAllCallback, BeforeEachCallback, BeforeTestExecutionCallback and their After* counterparts. In what order do these run around a single test method, and how does that order change when several extensions are registered?

level: middleimportance: must knowfreq 45%

basics

~10 s

Order is BeforeAllCallback, @BeforeAll, BeforeEachCallback, @BeforeEach, BeforeTestExecutionCallback, the test, AfterTestExecutionCallback, @AfterEach, AfterEachCallback, @AfterAll, AfterAllCallback. Multiple extensions nest like an onion: Before* in registration order, After* in reverse.

open as a page

In JUnit 5, how do you make a test class or a test method skip itself automatically based on a runtime check — for example, a required database not being reachable — using the extension model rather than commenting the test out?

level: middleimportance: must knowfreq 50%

basics

~20 s

Implement JUnit 5's ExecutionCondition extension. Its evaluateExecutionCondition(ExtensionContext) returns ConditionEvaluationResult.enabled(reason) or disabled(reason). JUnit calls it for every container and test it is registered on; a disabled result skips that node, reported as skipped rather than failed.

open as a page

How do you make JUnit 5 hand your test methods a ready-made collaborator — say a pre-configured HTTP client — as a method argument? Walk through the extension interface, its two methods, and how you keep it from claiming parameters it should not.

level: middleimportance: must knowfreq 45%

basics

~20 s

Implement ParameterResolver: supportsParameter(ParameterContext, ExtensionContext) returns true only for parameters you own, and resolveParameter(...) returns the object. Register it with @ExtendWith. Narrow supportsParameter by both a custom annotation and the parameter type so it never competes with another resolver.

open as a page

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%

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.

open as a page

A JUnit 5 test only makes sense on Linux and only when the environment variable CI is set. What does JUnit 5 give you out of the box to keep that test in the suite but not run it elsewhere, and how does the non-run show up in the results?

level: juniorimportance: should knowfreq 48%

basics

~20 s

Use JUnit 5's built-in conditional annotations: @EnabledOnOs(OS.LINUX) plus @EnabledIfEnvironmentVariable(named = "CI", matches = ".+"). Combine them on the class or method. Elsewhere the test is reported as skipped with a reason, not failed and not passed.

open as a page

JUnit 5 test methods may declare parameters even though you never pass arguments yourself, and the test still runs. Which parameter types does JUnit 5 supply out of the box, what is each one for, and how does JUnit know what to inject?

level: juniorimportance: should knowfreq 42%

basics

~20 s

JUnit 5 injects parameters through ParameterResolver extensions. Built in: TestInfo (display name, tags, test class/method), TestReporter (publish key/value entries into the report), and RepetitionInfo (current and total repetitions, inside a repeated test). Extensions can register resolvers for their own types.

open as a page

When would you implement JUnit 5's BeforeEachCallback/AfterEachCallback extension interfaces instead of just writing @BeforeEach and @AfterEach methods in the test class, and what do you gain?

level: middleimportance: should knowfreq 35%

basics

~20 s

Use callbacks when the setup is cross-cutting and reusable across many test classes, needs to wrap the tests' own setup, or must be composed with other extensions. Annotated methods are fine for fixture code specific to one class.

open as a page

You want a JUnit 5 extension that records the outcome of every executed test method — passed, failed, aborted, or skipped — into your own report. Which extension interface gives you that, and what exactly are its callbacks?

level: middleimportance: should knowfreq 38%

basics

~10 s

Implement JUnit 5's TestWatcher extension. It has four default methods: testSuccessful(context), testFailed(context, Throwable cause), testAborted(context, Throwable cause) and testDisabled(context, Optional<String> reason). Exactly one fires per executed test method, after its after-each callbacks.

open as a page

When would you register a JUnit 5 Jupiter extension programmatically with @RegisterExtension on a field instead of declaring it with @ExtendWith, and what rules apply to that field?

level: middleimportance: should knowfreq 38%

basics

~20 s

Use @RegisterExtension when you must build the extension instance yourself — constructor arguments or a builder — or when the test needs to query it (for example a randomly assigned port). The field must be non-private, of an Extension type, and non-null when JUnit reads it.

open as a page

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?

level: middleimportance: should knowfreq 30%

basics

~20 s

A 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.

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

JUnit Jupiter's @TempDir annotation accepts a cleanup attribute with the values ALWAYS, ON_SUCCESS and NEVER. What does each do, which is the default, and why would you change it?

level: middleimportance: should knowfreq 28%

basics

~20 s

ALWAYS (the default) deletes the directory when its scope ends whatever the outcome. ON_SUCCESS deletes it only if the test passed, keeping the files for inspection when it failed. NEVER always keeps it. You can also set the default globally with the junit.jupiter.tempdir.cleanup.mode.default configuration parameter.

open as a page

In a JUnit 5 test class, what is the difference in lifetime between a temp directory injected through a non-static @TempDir field and one injected through a static @TempDir field, and when would you accept the shared one?

level: middleimportance: should knowfreq 38%

basics

~20 s

A non-static @TempDir field is created fresh before each test and deleted after it. A static field (or a @BeforeAll parameter) gives one directory for the whole class, created before the first test and deleted after the last, so all tests share it and its accumulated contents.

open as a page

JUnit 5 provides TestExecutionExceptionHandler as an extension callback. What can it do with a failing test, what are its limits, and where would you use it responsibly?

level: seniorimportance: should knowfreq 30%

basics

~20 s

It intercepts a throwable from a @Test method body and may swallow it (return normally, test passes), rethrow it, or rethrow a different one. It does not see exceptions from @BeforeEach/@AfterAll — those need LifecycleMethodExecutionExceptionHandler.

open as a page

A colleague writes a JUnit 5 TestWatcher that re-runs failed tests and throws an exception when it cannot write its report file. What documented limits of TestWatcher make both of those a mistake, and what does JUnit do with an exception thrown from a watcher callback?

level: seniorimportance: should knowfreq 26%

basics

~20 s

A TestWatcher may not influence execution: its callbacks return void and run after the test has already finished and after its after-each callbacks, so it cannot retry or change an outcome. Exceptions thrown from a watcher callback are logged and otherwise ignored — the build stays green while data is silently lost.

open as a page

A JUnit 5 run fails before any test body executes, with a ParameterResolutionException. What are the distinct situations that produce that exception, and how do you diagnose and fix each one?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Four causes: no registered resolver supports a declared parameter; two resolvers both claim it (competing resolvers); the resolver returned a value incompatible with the declared type (or null for a primitive); or supportsParameter/resolveParameter itself threw. Fix by registering the extension, narrowing claims with a marker annotation, or correcting the returned type.

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

A test suite that injects directories with JUnit's @TempDir passes on Linux but fails on Windows CI with an IOException saying the temp directory could not be deleted. How do you diagnose and fix that?

level: seniorimportance: should knowfreq 22%

basics

~20 s

Almost always an unclosed file handle: Windows refuses to delete files that are still open, Linux does not. Find the stream, channel, ZipFile, watch service or memory-mapped buffer the test or the code under test leaves open and close it, normally with try-with-resources. Read-only file attributes are the other cause.

open as a page

JUnit 5's extension model includes InvocationInterceptor. How does it differ from the simple before/after callbacks, what must an implementation always do, and what problems does it solve that callbacks cannot?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

InvocationInterceptor wraps the invocation itself rather than bracketing it, receiving an Invocation object it must either proceed() or skip(). That lets it surround the call with try/catch/finally, retry it, or run it on another thread — things separate before/after callbacks cannot express.

open as a page

JUnit 5's extension model includes TestInstancePostProcessor. What is it for, when in the lifecycle does it run, and how does the per-class test instance lifecycle change its behaviour?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

TestInstancePostProcessor lets an extension act on a freshly created test instance — typically injecting into annotated fields. It runs right after instantiation, before BeforeEachCallback. With the default per-method lifecycle it runs for every test; with per-class lifecycle it runs once.

open as a page

A JUnit 5 suite has many tests switched off by @Disabled and by custom conditional annotations. Once a quarter you want one run that executes all of them without touching the code. What does the junit.jupiter.conditions.deactivate configuration parameter do, how do you supply it, and what are the caveats?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

junit.jupiter.conditions.deactivate takes a comma-separated pattern matched against the fully qualified class names of ExecutionCondition implementations; matching conditions are not evaluated, so their tests run. * deactivates all, including the one behind @Disabled. Supply it as a JVM system property, in junit-platform.properties, or via launcher config parameters.

open as a page

JUnit 5 can pick up extensions from the classpath automatically via the ServiceLoader. How is that turned on, what does an extension author have to publish, and why is it usually not the default choice?

level: seniorimportance: nice to knowfreq 16%

basics

~20 s

Set the configuration parameter junit.jupiter.extensions.autodetection.enabled=true (in junit-platform.properties, as a system property, or in the discovery request). The extension JAR must declare its class in META-INF/services/org.junit.jupiter.api.extension.Extension. It then applies to every test, which is why it is opt-in and off by default.

open as a page

How would you make JUnit Jupiter's @TempDir hand out directories from somewhere other than the default operating-system temp location — for example an in-memory filesystem or a fixed parent directory?

level: seniorimportance: nice to knowfreq 14%

basics

~20 s

Implement JUnit's TempDirFactory interface, whose createTempDirectory method returns the Path to use, and point at it with @TempDir(factory = MyFactory.class), or set it for the whole suite with the junit.jupiter.tempdir.factory.default configuration parameter. The injected field must be a Path, not a File.

open as a page

You are designing a reusable JUnit 5 extension that gives every integration test a clean database, a stubbed HTTP dependency and diagnostics on failure. How do you decide which callback interfaces to implement, and what failure modes do you design against?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Map each concern to lifecycle scope: expensive shared resources in BeforeAll/AfterAll callbacks, per-test isolation in BeforeEach/AfterEach callbacks, diagnostics via the exception handler plus AfterTestExecution. Design for teardown that always runs, deterministic ordering, and honest reporting.

open as a page

Your team's JUnit 5 tests all need a set of collaborators — clients, fixtures, seeded data. When is writing custom ParameterResolver extensions the right way to supply them, and when do you instead reach for a framework's DI extension or plain factory helpers in the test code?

level: principalimportance: nice to knowfreq 24%

basics

~20 s

Use a custom ParameterResolver when the same non-trivial collaborator is needed across many classes, its construction or cleanup is real work, or it must vary per injection point. Use plain helper factories for one-off or cheap objects, and an existing DI extension when a container already owns object graphs.

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