How does Spring's TestContext caching work, and what causes it to build a new context instead of reusing one?
answer
- static LRU cache, key = MergedContextConfiguration
- key includes classes, profiles, properties, @MockBean set, webEnvironment
- @DirtiesContext evicts & closes
- default maxSize = 32, LRU thrash
- converge on few shared configs
basics
~20 sSpring caches each ApplicationContext keyed by its configuration. Tests with an identical configuration reuse the cached context; any difference in the key (classes, profiles, properties, @MockBean set, web environment) triggers building a new, separately cached context.
solid answer
~40 sThe Spring TestContext framework maintains a static LRU cache of `ApplicationContext`s across the whole test run. The cache key (`MergedContextConfiguration`) is derived from everything that defines the context: `@ContextConfiguration`/`@SpringBootTest` classes & locations, active profiles (`@ActiveProfiles`), property sources (`@TestPropertySource`, `@DynamicPropertySource`), context initializers, `webEnvironment`, context customizers — and crucially the set of `@MockBean`/`@SpyBean` definitions. If two test classes produce the same key, they **share** one built context (huge speedup). Any difference forks a new context. Cache-busters: `@DirtiesContext` evicts/closes the context; sprinkling different `@MockBean` combinations or per-class properties multiplies contexts; the default cache size is 32 (`spring.test.context.cache.maxSize`) — exceeding it evicts by LRU, forcing rebuilds. The design goal is to converge on a *small number* of shared configurations.
code
java · 17 lines// These two SHARE one cached context (identical config key):
@SpringBootTest
class ContextA { /* ... */ }
@SpringBootTest
class ContextB { /* ... */ } // reuses ContextA's context
// This one FORKS a new context (different property + a mock):
@SpringBootTest(properties = "feature.x.enabled=true")
class ContextC {
@MockBean PaymentGateway gateway; // mock set is part of the cache key
}
// This one is a suite-wide tax: the context is closed & rebuilt around it.
@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
class MutatesSingletonState { /* ... */ }go deeper
Aware that Spring reuses contexts between tests to save time.
Knows identical configs share a context and that @DirtiesContext forces a rebuild.
Explains MergedContextConfiguration key components and actively engineers configs to maximize reuse.
Sets suite-wide standards (base classes, profile/property discipline, mock consolidation, maxSize tuning) and treats context count as a CI cost metric.
## The context cache Spring's **TestContext framework** (the thing behind `@SpringBootTest`, slices, `@ContextConfiguration`) builds an `ApplicationContext` for a test class and then **caches it statically** so subsequent test classes can reuse it instead of rebuilding. This is *the* reason a big Spring test suite isn't unbearably slow: you pay each unique context's startup **once**, not per class. ### The cache key: MergedContextConfiguration The cache is a `Map` keyed by a `MergedContextConfiguration` — a value object computed from **everything that affects what the context contains**: - The configuration **classes**/locations (`@SpringBootTest(classes=...)`, `@ContextConfiguration`, the detected `@SpringBootConfiguration`). - **Active profiles** — `@ActiveProfiles("foo")` vs none = different keys. - **Property sources** — `@TestPropertySource`, inlined `properties = {...}` on `@SpringBootTest`, `@DynamicPropertySource`. - **`ContextInitializer`s** and **ContextCustomizers**. - **`webEnvironment`** (MOCK vs RANDOM_PORT…). - The set of **`@MockBean` / `@SpyBean`** declarations (these are context customizers — different mock sets → different key). - Parent context, resource loader, etc. Two test classes whose `MergedContextConfiguration` is **equal** share the **same** cached context instance. ### What forks / busts the cache 1. **`@DirtiesContext`** — marks the context dirty; Spring **closes and removes** it from the cache (before/after class or method, per the mode). The next test rebuilds. Use sparingly — it's a suite-wide performance tax. 2. **Config variation** — different profiles, different `@TestPropertySource` values, different `@MockBean` combinations, different `webEnvironment` — each unique combination is a **separate** cached context. "Just one more property" silently doubles your startup cost. 3. **Cache eviction by size** — the cache is an **LRU** with default max size **32** (`spring.test.context.cache.maxSize`). If your suite produces more than 32 distinct contexts, least-recently-used ones are **closed and evicted**, so they get rebuilt when needed again — thrashing. ### How to keep caching effective - **Standardize** test configuration: shared base classes, one common `@ActiveProfiles`, a fixed set of properties. - **Consolidate `@MockBean` usage** or push mocking into slices where possible; identical mock sets reuse a context. - **Avoid `@DirtiesContext`** unless a test genuinely mutates singleton state that can't be reset otherwise. - Prefer **slices** — they naturally cluster into a few reusable configurations. - Monitor the count; raise `maxSize` only if you truly have many legitimate configs (better to reduce the count). ### Gotchas - The cache is **static/JVM-wide** for the test run and holds contexts open until eviction or JVM exit — beware resource leaks (open pools) with many contexts. - `@MockBean` is convenient but is a **cache fragmenter** — a class with a unique mock set can't share a context with others. - `@DynamicPropertySource` values participate in the key indirectly via the customizer; Testcontainers setups often each get their own context. - Ordering matters for LRU eviction — a large suite may rebuild the same context multiple times if it churns past `maxSize`.
- Why can @MockBean quietly slow down a large suite?Each distinct set of @MockBean/@SpyBean declarations is part of the context cache key, so a class with a unique mock combination can't reuse another class's cached context — it builds and caches its own, multiplying startup cost.
- When is @DirtiesContext justified despite its cost?When a test irreversibly mutates singleton/shared state (e.g., a bean's internal cache, a static registry, a modified BeanFactory) that would corrupt later tests reusing the same context, and no cheaper reset (like @Transactional rollback or resetting the mock) is available.
saying these in an interview costs you the question
- Believing a new context is built per test class or per test method regardless of config.
- Thinking @DirtiesContext is free or improves performance.
- Not realizing @MockBean/@TestPropertySource/@ActiveProfiles fork the context cache key.