You inherit a Spring Boot suite that takes 20 minutes because Spring reloads constantly. How do you diagnose and reduce the number of cached contexts?
answer
- Measure first: log org.springframework.test.context.cache
- Miss/eviction count = distinct contexts
- One base meta-annotation; consolidate mocks
- Static shared Testcontainer => same @DynamicPropertySource values
- Purge @DirtiesContext; maxSize last
basics
~20 sTurn on cache logging to count distinct contexts, then eliminate needless key differences: standardize one base configuration, consolidate mocks, share a static Testcontainer, and remove stray @DirtiesContext and @TestPropertySource variations so tests reuse a few contexts.
solid answer
~40 sFirst measure: enable DEBUG on `org.springframework.test.context.cache` to see cache size, hits, misses, and evictions — the miss/eviction count tells you how many distinct contexts you're building and whether you're thrashing past `maxSize` (default 32). Then attack the cache-key drivers: consolidate to a single shared base test configuration (a custom meta-annotation) so most classes hit one key; move scattered `@MockitoBean` sets into shared bases so mock customizers stop splitting the cache; make Testcontainers a single **static** container shared across classes so `@DynamicPropertySource` resolves to identical values; standardize profiles and `@TestPropertySource` so inlined properties don't vary; and audit `@DirtiesContext` usage, removing every case that could be handled by `@Transactional` rollback or explicit cleanup. The goal is a handful of distinct contexts reused across the whole suite. Raising `maxSize` is a last resort with a memory cost.
code
java · 21 lines// A single shared base so the WHOLE suite converges on one context.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootTest
@ActiveProfiles("test")
public @interface IntegrationTest { }
// Shared STATIC container -> identical @DynamicPropertySource values
// -> identical cache key across every test that extends this base.
public abstract class AbstractIntegrationTest {
static final PostgreSQLContainer<?> PG =
new PostgreSQLContainer<>("postgres:16");
static { PG.start(); } // started once per JVM, shared
@DynamicPropertySource
static void props(DynamicPropertyRegistry r) {
r.add("spring.datasource.url", PG::getJdbcUrl);
r.add("spring.datasource.username", PG::getUsername);
r.add("spring.datasource.password", PG::getPassword);
}
}go deeper
Recognize that reloading Spring repeatedly is the cost and that fewer distinct configs means faster tests.
Identify concrete key-splitters (mocks, properties, profiles) and consolidate them; know how to enable cache logging.
Systematically measure distinct contexts, standardize a base config, share a static container, and remove needless @DirtiesContext.
Establish suite-wide conventions and review gates that bound distinct contexts, treat new configurations as deliberate, and reason about the memory trade-off of maxSize.
## Step 1 — Measure before touching anything Enable DEBUG (or TRACE) logging for `org.springframework.test.context.cache`. The `ContextCache` logs statistics: **size**, **hitCount**, **missCount**, **parentContextCount**, and eviction activity. Interpretation: - **High miss count / many distinct sizes** ⇒ too many distinct cache keys (each miss = a full context build). - **Size pinned at `maxSize` (32) with ongoing evictions** ⇒ **thrashing**: reusable contexts are evicted and rebuilt. This turns a vague 'it's slow' into a number: *how many distinct contexts* the suite builds. ## Step 2 — Enumerate what splits the key Recall the key is `MergedContextConfiguration`: config classes, initializers, active profiles, property locations + inlined properties, context loader, parent, and **context customizers**. In a real suite the usual offenders are: 1. **Divergent `@MockitoBean`/`@MockBean` sets** — each unique set is a customizer ⇒ its own context. 2. **Per-class Testcontainers via `@DynamicPropertySource`** — different resolved URLs/ports ⇒ different keys. 3. **Ad-hoc `@TestPropertySource(properties=...)`** sprinkled per class. 4. **Inconsistent `@ActiveProfiles`**. 5. **`@SpringBootTest(webEnvironment=...)` mixing** MOCK and RANDOM_PORT. 6. **`@DirtiesContext`** forcing rebuilds even when a context would otherwise be reused. ## Step 3 — Converge on a few contexts - **One base configuration.** Create a project meta-annotation, e.g. `@IntegrationTest` composing `@SpringBootTest` + shared profiles + shared property source, and use it everywhere. Most classes now share one key. - **Consolidate mocks.** Push commonly mocked collaborators into a shared abstract base or a shared `@TestConfiguration` so the customizer set is identical across classes. Avoid one-off `@MockitoBean` in leaf classes when possible; consider real beans or a shared fake. - **Share a single static Testcontainer.** Declare the container `static` (started once per JVM), so `@DynamicPropertySource` resolves to the **same** values for every test ⇒ same key. This is the single biggest win in container-heavy suites. - **Standardize properties/profiles.** Move common test properties into `application-test.yml` under a single profile rather than per-class inlined properties. - **Slice appropriately.** Use `@WebMvcTest`, `@DataJpaTest`, etc. for narrow tests — but note each *slice type* is its own context; group tests by slice so each slice's context is heavily reused rather than mixing slices ad hoc. ## Step 4 — Purge unnecessary @DirtiesContext Audit every `@DirtiesContext`. For each, ask: is the context truly corrupted, or is this really per-test *data* cleanup? Replace with: - `@Transactional` test methods (automatic rollback) for DB state. - `@BeforeEach`/`@AfterEach` explicit reset. - Reliance on Spring's automatic **`@MockitoBean` reset between methods** (mocks are reset automatically; you rarely need to dirty for that). Keep `@DirtiesContext` only for genuine `ApplicationContext` corruption (bean permanently mutated, refresh/shutdown tests). ## Step 5 — Only then consider maxSize If, after consolidation, you still legitimately need more than 32 distinct contexts, raise `spring.test.context.cache.maxSize` (system property or `spring.properties`). Weigh the cost: every cached context stays alive holding beans, connection pools, and memory simultaneously; too high can exhaust RAM or DB connections in the test JVM. ## Step 6 — Guard against regression - Keep cache-stats logging available (or assert on it in a smoke test). - Add review conventions: `@DirtiesContext`, new `@TestPropertySource` variants, and per-class containers require justification. - Prefer the shared base annotation; treat a new distinct configuration as a deliberate decision. ## Mental model Suite speed ≈ (number of **distinct** contexts built) × (context startup cost) + test execution. You can't cheaply cut startup cost per context, so the lever is **reducing distinct contexts** and **reusing** them — the cache does the rest.
- Why does making the Testcontainer static rather than per-class dramatically cut the number of contexts?A static container starts once and exposes stable JDBC URL/port, so @DynamicPropertySource resolves to identical values for every test class; identical values mean identical cache keys, so all those classes share one cached context instead of each building its own.
- The team wants to just set maxSize to 200. What's your counsel?It hides the real issue and every cached context stays alive holding beans, pools, and memory at once — risking OOM or connection exhaustion in the test JVM. First drive down distinct contexts; raise maxSize only for a residual, genuinely necessary count, with the memory trade-off understood.
- How do slices like @WebMvcTest interact with this?Each slice type builds its own (smaller, faster) context, and distinct slice configurations are distinct keys. They help by being cheap and reusable, but mixing many slice variants still creates many keys, so group and standardize them too.
saying these in an interview costs you the question
- Jumping straight to raising maxSize without measuring distinct contexts
- Not knowing cache statistics are observable via logging
- Leaving per-class (non-static) Testcontainers that defeat caching
- Treating @DirtiesContext as normal rather than exceptional
- Assuming the slow part is test execution rather than repeated context startup