skip to content

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%

answer

  1. wraps the call: Invocation<T> in your hands
  2. must proceed() or skip() exactly once — else JUnit errors
  3. ReflectiveInvocationContext: method, args, target instance
  4. enables try/finally, context scoping, thread hop
  5. interceptors nest; heaviest hook — prefer callbacks when bracketing suffices

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.

solid answer

~50 s

Before/after callbacks are two separate notifications; the framework calls the target between them and you cannot influence that call. `InvocationInterceptor` instead **wraps** it: each method (`interceptTestMethod`, `interceptBeforeEachMethod`, `interceptTestFactoryMethod`, `interceptTestClassConstructor`, and so on) receives an `Invocation<T>` plus a `ReflectiveInvocationContext` describing the target method, its arguments and the target instance. The contract: you **must** call `invocation.proceed()` or `invocation.skip()` exactly once. Failing to do either makes JUnit fail the test with an error saying the invocation was never used, which prevents an interceptor from silently dropping a test. Because the call sits inside your method, you can wrap it in `try`/`catch`/`finally`, retry it, time it, run it inside a security or transaction context, or delegate it to a different thread — control flow that two independent callbacks fundamentally cannot express. Costs: interceptors compose as nested wrappers, so ordering matters more; they can swallow or alter outcomes; and thread hops lose thread-bound state.

code

java · 14 lines
java
class ImpersonationExtension implements InvocationInterceptor {
    @Override
    public void interceptTestMethod(Invocation<Void> invocation,
                                    ReflectiveInvocationContext<Method> ctx,
                                    ExtensionContext ext) throws Throwable {
        Principal previous = Principals.current();
        Principals.set(new Principal("test-admin"));
        try {
            invocation.proceed();          // exactly one proceed(), on every path
        } finally {
            Principals.set(previous);
        }
    }
}

go deeper

for a junior

Say it wraps the invocation instead of bracketing it, and that you must call proceed() or skip().

for a middle

Add the ReflectiveInvocationContext details, the interception points beyond @Test, and why the proceed/skip contract is enforced.

for a senior

Give concrete use cases (scoped execution, thread delegation, around-advice instrumentation), discuss nesting order, and explain when a simpler hook is the better choice.

for a principal

Weigh the power against suite trustworthiness: interceptors can hide failures and skip tests, so argue for thin single-purpose implementations, deliberate layering, and preferring conditions or callbacks where they suffice.

## Bracketing versus wrapping A `BeforeEachCallback` and an `AfterEachCallback` are two independent notifications. JUnit calls the first, then invokes the target, then calls the second. Your code never has the invocation in its hands, so you cannot put it inside a `try`, decide not to run it, run it twice, or run it somewhere else. `InvocationInterceptor` inverts that. Its methods receive the invocation as an object: ```java default void interceptTestMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable { invocation.proceed(); } ``` All methods have defaults that simply proceed, so you override only the phases you care about. There are interception points for the test class constructor, `@BeforeAll`/`@BeforeEach`/`@AfterEach`/`@AfterAll` methods, `@Test` methods, `@TestTemplate` invocations, `@TestFactory` methods, and dynamic tests. ## The mandatory contract Every interception method must call **exactly one** of: - `invocation.proceed()` — run the target and return its result; - `invocation.skip()` — do not run it, and report it as executed successfully (used, for example, by extensions that execute the body themselves in a different environment). If neither is called, JUnit raises an error stating that the invocation was never proceeded or skipped. This deliberate strictness stops a buggy interceptor from making a test silently not run — a failure mode that would be almost impossible to notice from a green report. `ReflectiveInvocationContext` supplies the reflective details: the executable being invoked, the resolved arguments, and the target instance (an `Optional`, empty for static targets). That is how an interceptor can, for example, inspect an annotation on the method or log the actual arguments. ## What it makes possible **Retry.** Catch a throwable from `proceed()` and call it again. (Note that for a `@Test` method the invocation object can be proceeded only once, so genuine retry usually needs a `TestTemplateInvocationContextProvider` producing multiple invocations; the interceptor is the right place for the surrounding policy, and this subtlety is exactly why retry is harder than it looks.) **Execution in a different context.** Run the invocation inside a transaction, under an impersonated security principal, with a populated logging MDC, or holding a lock — anything requiring the call to be *inside* a scope rather than merely preceded by one. **Execution on a different thread.** Some frameworks require test code to run on a specific thread — a UI toolkit's event dispatch thread, for instance. An interceptor can submit `proceed()` to that thread and wait for the result, rethrowing what it produced. This is the same trade-off as preemptive timeouts: anything held in a `ThreadLocal` on the original thread (transaction bindings, security context, MDC) will not be visible on the new one unless you propagate it explicitly. **True around-advice instrumentation.** Timing that must include exception paths, resource acquisition released in a `finally`, or capturing the arguments and outcome together in one place. **Conditional skipping with custom logic.** Deciding at invocation time not to run the body. Prefer `ExecutionCondition` when the decision can be made before the test starts, because a condition reports the test as skipped with a reason, which is the honest signal; `skip()` reports success. ## Composition and ordering Multiple interceptors nest: the first-registered wraps the others, so its `proceed()` triggers the next interceptor rather than the target directly. That gives clean layering (transaction outside, timing inside) but also means an interceptor that swallows an exception hides it from everything outside it. Keep interceptors thin and single-purpose, and be explicit about registration order when layering matters. ## When *not* to use it It is the heaviest hook in the extension model. If simple bracketing suffices — set something up, tear it down — use `BeforeEachCallback`/`AfterEachCallback`, which are simpler to reason about and cannot accidentally drop an invocation. If you only need to react to a failure, `TestExecutionExceptionHandler` or `TestWatcher` are more appropriate. If you need to decide whether to run at all based on the environment, use `ExecutionCondition`. Reach for `InvocationInterceptor` when you genuinely need the call inside your own control flow. ## Review checklist for an interceptor - Does every path call `proceed()` or `skip()` exactly once, including exception paths? - Are exceptions propagated rather than swallowed, unless swallowing is the explicit, documented purpose? - If it hops threads, is thread-bound context propagated or documented as lost? - Is the cost acceptable given it runs for every intercepted invocation in the suite? - Is its position relative to other interceptors deliberate rather than accidental?

  • What happens if an InvocationInterceptor implementation returns without calling proceed() or skip()?
    JUnit fails the affected test with an error reporting that the invocation was never proceeded or skipped. The strictness is deliberate: without it, a buggy interceptor could cause tests never to run while the report stayed green, which is a far worse outcome than a loud failure.
  • An interceptor runs the test body on a UI toolkit's dedicated thread. What must you be careful about?
    Anything held in a ThreadLocal on the original test thread — a Spring transaction and its bound persistence context, the security context, logging MDC — is invisible on the target thread, so transactional rollback and authentication can break exactly as they do with preemptive timeouts. You must also propagate the outcome correctly, rethrowing on the test thread whatever the body threw, and ensure you still call proceed() exactly once.

Before/after callbacks are two doorbells rung on either side of a room you never enter. An interceptor hands you the room: you decide when to open it, whether to open it at all, and what to wrap around what happens inside.

saying these in an interview costs you the question

  • Forgetting to call proceed() or skip() on an exception path
  • Believing an interceptor is just a fancier before/after pair
  • Swallowing exceptions inside an interceptor and reporting success
  • Ignoring that thread-bound context is lost when the invocation is delegated to another thread

context