skip to content

How does @ContextConfiguration inheritance/merging combine with the context cache key, and how do you control it?

level: principalimportance: should knowfreq 25%

answer

  1. MergedContextConfiguration folds classes/locations/initializers/profiles/props/loader/parent
  2. MCC equals() = context cache key
  3. inheritLocations / inheritInitializers default true (append vs replace)
  4. default cache max 32, LRU; @MockBean sets fork contexts
  5. @DirtiesContext evicts; converge configs to reuse

basics

~20 s

All declared config (classes, locations, initializers, active profiles, property sources, loader, parent) is combined into a MergedContextConfiguration. Its equality is the cache key, so identical configs share one context. Superclass config is merged by default; inheritLocations/inheritInitializers=false to override.

solid answer

~40 s

Every test's context config is normalized into a `MergedContextConfiguration`: it folds together `classes`, `locations`, `initializers`, active profiles (`@ActiveProfiles`), property sources (`@TestPropertySource`), the resolved `ContextLoader`, and any parent (hierarchy). Config declared on **superclass** tests is **merged in by default** — `inheritLocations` and `inheritInitializers` (both default `true`) control whether subclass config **adds to** or **replaces** the inherited config. That merged object's `equals()`/`hashCode()` is the **context cache key** in the `ContextCache`, so two tests with equal merged config **reuse one ApplicationContext**. This is the dominant performance lever in a Spring suite. Anything that changes the key — a different profile, an extra `@MockBean`, a different property, a new initializer — forks a **separate** cached context, and too many distinct keys blows the default cache (max 32, LRU-evicted) and rebuilds contexts. `@DirtiesContext` deliberately evicts a key.

code

java · 22 lines
java
// Base test: one shared configuration -> one cached context.
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = CoreConfig.class)
@ActiveProfiles("test")
abstract class AbstractIntegrationTest { }

// Reuses the SAME MergedContextConfiguration -> SAME cached context.
class OrderServiceTest extends AbstractIntegrationTest {
    @Autowired OrderService service;
}

// inheritLocations=true (default): appends ExtraConfig to CoreConfig,
// producing a DIFFERENT MCC -> a SEPARATE cached context.
@ContextConfiguration(classes = ExtraConfig.class)
class ReportingTest extends AbstractIntegrationTest {
    // beans from CoreConfig AND ExtraConfig are present here
}

// A lone @MockBean also forks the cache key even though CoreConfig is identical:
class PaymentTest extends AbstractIntegrationTest {
    @MockBean PaymentGateway gateway; // new MCC -> new cached context
}

go deeper

for a junior

Know that identical test config reuses one cached context instead of rebuilding.

for a middle

Explain that config is merged into a MergedContextConfiguration used as the cache key, and that profiles/properties change it.

for a senior

Discuss inheritLocations/inheritInitializers merging, @MockBean forking contexts, and @DirtiesContext eviction.

for a principal

Govern context proliferation at suite scale: cache max size/LRU thrash, converging configs, hierarchy parents for shared infra, and disciplined @DirtiesContext use.

## MergedContextConfiguration: the normalized recipe Whatever you scatter across annotations is collapsed by the TestContext Framework into a single **`MergedContextConfiguration`** (MCC). It aggregates: - **`classes`** and **`locations`** (component classes + XML/Groovy resources), after default detection. - **`initializers`** (`ApplicationContextInitializer` set). - **active profiles** from `@ActiveProfiles`. - **property sources** from `@TestPropertySource` (locations + inlined properties). - the resolved **`ContextLoader`**. - **`parent`** MCC when inside a `@ContextHierarchy`. - (for Boot) additional attributes contributed by `SpringBootContextLoader` such as `@MockBean`/`@SpyBean` definitions, web-environment mode, etc. ## Inheritance & merging semantics Spring test config **inherits down a test class hierarchy**: - **`@ContextConfiguration(inheritLocations = true)`** (default): the subclass's `classes`/`locations` are **appended** to those declared on superclasses. - **`inheritLocations = false`**: the subclass **replaces** inherited resources. - **`@ContextConfiguration(inheritInitializers = true)`** (default): subclass initializers are **merged** with superclass ones; `false` replaces. - **`@ActiveProfiles(inheritProfiles = ...)`** and **`@TestPropertySource(inheritLocations/inheritProperties = ...)`** have analogous switches. Ordering: inherited (superclass) config typically comes **before** the subclass's own, which matters when later definitions override earlier ones. ## The cache key The MCC implements **`equals()`/`hashCode()`** over all those attributes. The `org.springframework.test.context.cache.ContextCache` keys cached `ApplicationContext` instances by MCC. Therefore: - **Equal MCC -> same cached context reused** across test classes (built once). This is the single biggest reason a well-structured suite is fast. - **Any differing attribute -> a distinct key -> a separate context** built and cached. Common accidental key-forks: - Different `@ActiveProfiles`. - An extra inlined `@TestPropertySource` property. - A different set of `@MockBean`/`@SpyBean` (each unique mock combination is a new context under Boot). - A different initializer or a different `@ContextConfiguration` classes list. ## Cache sizing & eviction The default `ContextCache` holds **up to 32** contexts (`spring.test.context.cache.maxSize`), evicting **LRU**. A suite with dozens of distinct MCCs thrashes the cache — contexts get evicted and **rebuilt**, and each rebuild is expensive (full refresh, connection pools, embedded infra). Symptom: slow suites where the *same* context seems to rebuild repeatedly. `@DirtiesContext` **explicitly evicts** the current context's key (optionally the whole hierarchy subtree via `hierarchyMode`), forcing a rebuild for the next test that needs it — use sparingly because it defeats caching. ## Practical governance (principal-level) - **Converge on a small number of shared test configurations** (a base test class or a few `@…Test` slice annotations) so most tests hit the same MCC. - **Avoid needless per-test `@MockBean`/property variation**; it multiplies contexts. - **Push expensive shared infra into a parent context** via `@ContextHierarchy` so it's cached once across many child configs. - **Reserve `@DirtiesContext`** for genuine mutation-of-context cases; prefer resetting state (e.g., `@Transactional` rollback, mock resets) instead. - Consider **raising `spring.test.context.cache.maxSize`** only after understanding *why* there are many distinct configs — the better fix is usually fewer configs. ## Gotchas - Two configs that look textually different but normalize to the same MCC **do** share a context (and vice versa) — reason about the *merged* result, not the annotations. - Merged inheritance can **silently pull in** a superclass's config you forgot about, changing the key and the beans present. - Field/parameter injection of `@MockBean` changes the MCC even if the production config is identical — a frequent hidden cause of context proliferation.

  • Why can adding a single @MockBean to one test slow the whole suite?
    @MockBean contributes to MergedContextConfiguration, so it changes the cache key and forks a new ApplicationContext. Many unique mock combinations create many contexts, thrash the 32-entry LRU cache, and force repeated expensive rebuilds.
  • What do inheritLocations and inheritInitializers control?
    Whether a subclass test's config is appended to (true, default) or replaces (false) the classes/locations and initializers declared on superclass tests, which in turn changes the resulting MergedContextConfiguration and cache key.
  • How would you reduce context rebuilds in a large suite?
    Converge tests onto a few shared configurations (base classes/slice annotations), minimize per-test @MockBean/property variation, push expensive infra into a cached @ContextHierarchy parent, and reserve @DirtiesContext for genuine context mutation rather than routine cleanup.

saying these in an interview costs you the question

  • Believing every test gets its own fresh ApplicationContext by default (contexts are cached and shared)
  • Thinking @MockBean or @ActiveProfiles doesn't affect the cache key
  • Overusing @DirtiesContext and then wondering why the suite is slow
  • Reasoning about annotations textually instead of the merged MCC that actually keys the cache

context