skip to content

Under the hood, which JUnit 5 callback interfaces does SpringExtension implement, and how does that plug into TestContextManager and context caching?

level: principalimportance: nice to knowfreq 25%

answer

  1. SpringExtension = adapter; TestContextManager = engine
  2. Callbacks: BeforeAll/Each, TestInstancePostProcessor, ParameterResolver
  3. TCM in JUnit ExtensionContext Store, one per class
  4. ContextCache keyed by MergedContextConfiguration (default 32, LRU)
  5. @MockBean/@DirtiesContext => new/evicted context

basics

~10 s

SpringExtension implements several JUnit 5 callbacks — BeforeAll/AfterAll, BeforeEach/AfterEach, TestInstancePostProcessor, and ParameterResolver (plus TestExecutionExceptionHandler). Each callback delegates to a TestContextManager, which drives TestExecutionListeners and manages the cached ApplicationContext keyed by merged configuration.

solid answer

~40 s

SpringExtension is the adapter between JUnit Jupiter callbacks and Spring's TestContextManager. It implements BeforeAllCallback/AfterAllCallback, BeforeEachCallback/AfterEachCallback, TestInstancePostProcessor (field + constructor DI), ParameterResolver (parameter injection), and TestExecutionExceptionHandler. Each callback obtains a TestContextManager stored in JUnit's ExtensionContext Store (one per test class) and calls the matching hook: beforeTestClass, prepareTestInstance, beforeTestMethod/afterTestMethod, etc. TestContextManager fans those out to registered TestExecutionListeners — DependencyInjectionTestExecutionListener, DirtiesContextTestExecutionListener, TransactionalTestExecutionListener, and others. The ApplicationContext lives in a static ContextCache keyed by a MergedContextConfiguration (config classes, active profiles, initializers, etc.), so identical configurations across many test classes share one context; @DirtiesContext evicts it. This caching, plus the Store scoping, is why SpringExtension is efficient and why context config is the cache key.

code

java · 12 lines
java
// Two classes with IDENTICAL merged config share ONE cached context.
@SpringJUnitConfig(AppConfig.class)
class AlphaTest { @Autowired ApplicationContext ctx; /* ... */ }

@SpringJUnitConfig(AppConfig.class)
class BetaTest  { @Autowired ApplicationContext ctx; /* same cached ctx as AlphaTest */ }

// Adding @MockBean changes MergedContextConfiguration -> its OWN cached context,
// and @DirtiesContext evicts a context, forcing a rebuild.
@SpringJUnitConfig(AppConfig.class)
@org.springframework.test.annotation.DirtiesContext
class GammaTest { /* context evicted after this class */ }

go deeper

for a junior

Awareness that Spring reuses (caches) the context between tests is enough.

for a middle

Know the context is cached by configuration and @DirtiesContext rebuilds it.

for a senior

Explain TestContextManager/TestExecutionListeners and how @MockBean/@ActiveProfiles affect the cache key.

for a principal

Reason about the full callback-to-engine mapping, ContextCache sizing/eviction, parallelism, and how config choices drive context proliferation in large suites.

## The adapter role `SpringExtension` is deliberately thin: it translates JUnit 5's lifecycle callbacks into calls on a **`TestContextManager`**, the long-standing heart of the Spring TestContext Framework (TCF) that predates JUnit 5. ### Interfaces it implements From `org.junit.jupiter.api.extension`: - **`BeforeAllCallback` / `AfterAllCallback`** → `TestContextManager.beforeTestClass()` / `afterTestClass()`. - **`TestInstancePostProcessor`** → `TestContextManager.prepareTestInstance()` — performs **field and constructor dependency injection** into the freshly created test instance. - **`BeforeEachCallback` / `AfterEachCallback`** → `beforeTestMethod()` / `afterTestMethod()`. - Also `BeforeTestExecutionCallback` / `AfterTestExecutionCallback` → `beforeTestExecution()` / `afterTestExecution()` (finer-grained, around the actual test body). - **`ParameterResolver`** → resolves constructor/method parameters as beans (the injection discussed elsewhere). - **`TestExecutionExceptionHandler`** → lets listeners react to test exceptions (e.g. transaction rollback semantics). ### Where the TestContextManager lives JUnit 5 gives each `ExtensionContext` a namespaced **`Store`**. `SpringExtension` lazily creates one `TestContextManager` per top-level test class and stashes it in the class-level `Store`. Every callback retrieves that same manager, so state (the `TestContext`, the active listeners) is consistent across the class's lifecycle. ### TestExecutionListeners `TestContextManager` holds an ordered chain of `TestExecutionListener`s, discovered via `spring.factories`/`@TestExecutionListeners`. The important defaults: - `DependencyInjectionTestExecutionListener` — actually performs `@Autowired`/injection during `prepareTestInstance`/`beforeTestMethod`. - `DirtiesContextTestExecutionListener` (+ `...BeforeModesTestExecutionListener`) — marks the cached context dirty for eviction. - `TransactionalTestExecutionListener` — starts/rolls back a transaction around `@Transactional` tests. - `SqlScriptsTestExecutionListener`, `EventPublishingTestExecutionListener`, `ApplicationEventsTestExecutionListener`, `ServletTestExecutionListener`, `MockitoTestExecutionListener` (Boot), etc. ## Context caching — the performance story The `ApplicationContext` is **not** rebuilt per test. The TCF keeps a process-wide **`ContextCache`** (default max 32 contexts, LRU-evicted). The cache **key** is a **`MergedContextConfiguration`**: the merged result of config classes/locations, `ContextInitializer`s, active profiles (`@ActiveProfiles`), property sources (`@TestPropertySource`), context customizers (e.g. `@MockBean`, `@DynamicPropertySource` differences), and parent context. Two test classes with an **identical** merged configuration **share one context**; any difference produces a separate cached context. Consequences: - **`@DirtiesContext`** removes the context from the cache (before or after the class/method per its `methodMode`/`classMode`), forcing a rebuild for the next user — expensive, use sparingly. - **`@MockBean`/`@SpyBean`** (Boot) change the merged config via a `ContextCustomizer`, so a class using them gets its *own* cached context rather than sharing the vanilla one — a common cause of context proliferation and slow suites. - Choosing consistent profiles/property sources across tests maximizes cache hits. ## Why this design - **Composability**: because these are discrete JUnit 5 interfaces, `SpringExtension` coexists with other extensions — impossible under JUnit 4's single Runner. - **Separation**: JUnit-facing adapter (`SpringExtension`) vs engine (`TestContextManager` + listeners) means the same TCF engine powers JUnit 4 (`SpringRunner`), JUnit 5, and TestNG (`AbstractTestNGSpringContextTests`). ## Gotchas for a principal - Static `ContextCache` means leaked/large contexts persist across a JVM test run; watch memory and the 32-context default (`spring.test.context.cache.maxSize`). - Parallel test execution needs care: the `ContextCache` is thread-safe but `@DirtiesContext` + parallelism can thrash. - `@Nested` classes reuse the enclosing class's `TestContextManager`/context by default. - Ordering: `TestExecutionListener` order matters (dependency injection before transaction start, etc.).

  • Why does adding @MockBean often slow a large test suite?
    @MockBean registers a ContextCustomizer that becomes part of the MergedContextConfiguration cache key. Classes using different mock setups no longer match the vanilla cached context, so Spring builds and caches additional contexts — more startups, more memory, and possible LRU eviction of otherwise-reusable contexts (default cache size 32).
  • Which component actually performs the @Autowired injection — SpringExtension itself?
    Not directly. SpringExtension's TestInstancePostProcessor/BeforeEach callbacks delegate to TestContextManager, which invokes DependencyInjectionTestExecutionListener; that listener performs the autowiring against the cached ApplicationContext. SpringExtension is just the JUnit-facing adapter.

saying these in an interview costs you the question

  • Saying a fresh ApplicationContext is built for every test method/class
  • Claiming SpringExtension performs injection itself rather than delegating to TestExecutionListeners
  • Not knowing the context cache key is the merged configuration (thinking it's keyed only by config class name)
  • Believing @DirtiesContext is cheap/harmless

context