skip to content

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%

answer

  1. postProcessTestInstance(instance, context) — right after construction
  2. before BeforeEachCallback and @BeforeEach
  3. fields only; parameters need ParameterResolver
  4. @Mock / Spring autowiring are the canonical users
  5. PER_CLASS ⇒ runs once, injected state shared

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.

solid answer

~50 s

`TestInstancePostProcessor` has one method, `postProcessTestInstance(Object testInstance, ExtensionContext context)`, called immediately after JUnit creates the test class instance and before any `BeforeEachCallback` or `@BeforeEach`. Its job is to act on the object itself — almost always **field injection**: scanning declared fields for an annotation and setting them reflectively. `MockitoExtension` initialising `@Mock` fields and Spring's `SpringExtension` performing dependency injection into the test instance are the canonical examples. Lifecycle matters. With the default `Lifecycle.PER_METHOD`, JUnit creates a new instance for each test method, so the post-processor runs **once per test** and each test gets fresh fields. With `@TestInstance(Lifecycle.PER_CLASS)`, one instance serves the whole class, so it runs **once** — injected mocks or fixtures are then shared and mutated state leaks between tests unless you reset them in `BeforeEachCallback`. Remember it only reaches fields. Parameters of test or lifecycle methods are supplied by `ParameterResolver`; many extensions implement both so either style works.

code

java · 15 lines
java
class FixedClockExtension implements TestInstancePostProcessor {
    @Override
    public void postProcessTestInstance(Object instance, ExtensionContext ctx) throws Exception {
        for (Field f : instance.getClass().getDeclaredFields()) {
            if (f.isAnnotationPresent(FixedClock.class)) {
                if (Modifier.isFinal(f.getModifiers())) {
                    throw new ExtensionConfigurationException(
                            "@FixedClock field must not be final: " + f.getName());
                }
                f.setAccessible(true);
                f.set(instance, Clock.fixed(Instant.parse("2020-01-01T00:00:00Z"), ZoneOffset.UTC));
            }
        }
    }
}

go deeper

for a junior

Know that it is the hook that fills in annotated fields on the test object right after it is constructed, before any setup method runs.

for a middle

Add that it cannot touch method parameters (ParameterResolver does) and that the default per-method lifecycle means one run per test.

for a senior

Discuss the PER_CLASS interaction and state leakage, resetting in BeforeEachCallback, failing fast on misconfigured fields, and inherited or nested-class fields.

for a principal

Frame it as an injection contract for the test tier: what the extension owns, how it degrades under alternative lifecycles, and how it composes with DI-driven instance factories.

## The interface ```java public interface TestInstancePostProcessor extends Extension { void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception; } ``` JUnit hands you the just-constructed test class instance. You may do anything with it; in practice extensions use reflection to populate fields. ## Where it sits in the lifecycle For the default per-method instance lifecycle, the sequence around one test is: 1. `BeforeAllCallback`, then `@BeforeAll` (container level, no instance yet for per-method lifecycle) 2. the test class instance is created (optionally by a `TestInstanceFactory`) 3. **`TestInstancePostProcessor`** runs 4. `BeforeEachCallback` 5. `@BeforeEach` 6. `BeforeTestExecutionCallback`, the test body, and the unwinding After* chain So the post-processor is the earliest point at which an extension can touch the object, and it is guaranteed to have run before any user setup — a test's `@BeforeEach` can safely use an injected field. When several extensions implement it, they run in registration order, like other before-style callbacks. ## What it is used for **Field injection.** The dominant use. Walk `testInstance.getClass().getDeclaredFields()` (including inherited fields), find those carrying your annotation, build or look up the value, `setAccessible(true)`, and assign. Real-world examples: - Mockito's extension creating mocks and spies for `@Mock`/`@Spy` fields and wiring `@InjectMocks`. - Spring's `SpringExtension` performing autowiring into the test instance from the test `ApplicationContext`. - Home-grown extensions injecting a stub client, a fixed `Clock`, a tenant id or a generated test-data builder. **Registering the instance somewhere.** For example putting a reference into the `ExtensionContext` store so later callbacks (or an exception handler) can read fields off it, or registering the instance with an event bus that the code under test publishes to. **Validation.** Failing fast with a clear message when the test class is mis-declared — a required field is missing, an annotation is on a `final` or `static` field, two mutually exclusive annotations are present. Throwing here produces a failure before any test body runs, which is much clearer than a `NullPointerException` inside a test. ## Fields versus parameters `TestInstancePostProcessor` can only reach **fields of the test instance**. It cannot supply arguments to a test method, a constructor or a lifecycle method — that is `ParameterResolver`'s job. Many extensions implement both so users can write either ```java @Mock Repository repo; // field injection void test(@Mock Repository repo) // parameter resolution ``` It also cannot inject into `static` fields for a per-method lifecycle class in any meaningful shared way; class-level resources belong in `BeforeAllCallback` plus the store. ## The per-class lifecycle interaction By default (`Lifecycle.PER_METHOD`) JUnit constructs a new test instance for every test method. Consequences: the post-processor runs once per test, injection is repeated, and each test starts with fresh field values — the isolation most people assume. Annotating the class `@TestInstance(Lifecycle.PER_CLASS)` — commonly done so `@BeforeAll` can be non-static or so a `@TestFactory`/`@MethodSource` can be an instance method — creates **one instance for the whole class**. Then: - the post-processor runs **once**, before the first test; - injected objects are shared by every test in the class; - any mutation a test makes to an injected field (stubbings recorded on a mock, entries added to a collection, a mutated builder) is visible to subsequent tests, making them order-dependent; - an extension that assumed "one injection per test" silently changes behaviour when a user adds that annotation. A robust extension therefore either resets shared state in `BeforeEachCallback` (for mocks: re-create or reset them per test), or explicitly detects the lifecycle via the `ExtensionContext`'s test-instance lifecycle accessor and adapts — or documents that it requires the per-method lifecycle. ## Related hooks worth knowing - `TestInstanceFactory` — replaces instance *creation* entirely (used when a DI container must construct the test class). At most one may be registered per class. - `TestInstancePreDestroyCallback` — the symmetric hook to clean up an instance before it is discarded, useful for closing injected resources. - `TestInstances` on the `ExtensionContext` — for `@Nested` classes, gives access to the enclosing instances as well as the innermost one, which matters when injecting into nested hierarchies. ## Practical cautions Reflection into a test class is inherently coupling: prefer a narrow, annotation-driven contract and fail loudly when the annotation is used on something you cannot handle. Remember inherited and `@Nested`-enclosing fields exist. And keep the post-processor cheap — with the per-method lifecycle it runs for every single test in the suite.

  • An extension injects a mock via TestInstancePostProcessor. A user adds @TestInstance(Lifecycle.PER_CLASS) to the class and tests start interfering with each other. Why, and what should the extension do?
    With the per-class lifecycle a single test instance serves every method, so the post-processor runs once and the injected mock is shared; stubbings and recorded invocations from one test leak into the next, making results order-dependent. The extension should reset or re-create the injected state in BeforeEachCallback, or detect the lifecycle from the ExtensionContext and either adapt or fail with a clear configuration error.
  • Why do many extensions implement both TestInstancePostProcessor and ParameterResolver?
    The post-processor can only populate fields on the test instance, so method parameters would remain unresolved; ParameterResolver covers arguments to constructors, lifecycle methods and test methods. Supporting both lets users choose field injection for values shared across a class's tests and parameter injection for values scoped to a single test, without the extension caring which style they picked.

saying these in an interview costs you the question

  • Thinking TestInstancePostProcessor can inject test method parameters
  • Believing it always runs once per class regardless of the instance lifecycle
  • Assuming it runs after @BeforeEach rather than before it
  • Injecting mutable shared state without resetting it per test under PER_CLASS

context