How do @ActiveProfiles and @TestPropertySource affect the TestContext context cache, and why can careless use slow a large test suite?
answer
- MergedContextConfiguration = cache key
- Profiles (as set) + property sources both in the key
- Distinct combo => new full startup
- @MockBean / @DynamicPropertySource also fork cache
- Share via base class / files, not per-class inline
basics
~20 sThe set of active profiles and the test property sources are both part of the key the TestContext Framework uses to cache ApplicationContexts. Any difference produces a distinct key, so a new context is built instead of reusing a cached one — many unique combinations mean many expensive context startups.
solid answer
~40 sSpring caches loaded ApplicationContexts across test classes to avoid re-starting the container for every test. The cache key (MergedContextConfiguration) is composed of everything that could change the context: config classes/locations, active profiles from @ActiveProfiles, the property sources and inlined properties from @TestPropertySource, context initializers, context customizers, and so on. Two test classes share a cached context only if all of these match. So each distinct @ActiveProfiles set or each unique inlined-property string creates a separate context. In a big suite, sprinkling one-off @TestPropertySource(properties=...) values or ad-hoc profile combinations quietly multiplies the number of contexts, and each new context pays full Spring/Boot startup cost. The fix: standardize on a small number of shared configurations, push overrides into shared property files or a common base test class, and reserve per-class inlined properties for genuinely unique cases.
code
kotlin · 16 lines// GOOD: one shared meta-annotation -> one cache key -> one context
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@SpringBootTest
@ActiveProfiles("test")
@TestPropertySource(locations = ["classpath:/config/it.properties"])
annotation class IntegrationTest
@IntegrationTest class OrderIT { /* shares context */ }
@IntegrationTest class PaymentIT { /* shares the SAME cached context */ }
// RISKY: per-class inlined override -> distinct key -> its own context
@SpringBootTest
@ActiveProfiles("test")
@TestPropertySource(properties = ["app.rate=99"]) // forks the cache just for this class
class RateEdgeCaseITgo deeper
Know contexts are cached to speed tests.
Know profiles and property overrides change which context is reused.
Explain MergedContextConfiguration and the main key contributors.
Architect shared setups (base class/meta-annotation), diagnose cache thrash, and trade off per-class overrides against startup cost.
## Why caching exists Starting a Spring/Boot `ApplicationContext` is **expensive** — component scanning, auto-configuration, connection pools, embedded servers. The **Spring TestContext Framework** caches contexts so multiple test classes that need the *same* context reuse one instance instead of rebuilding it. Default cache size is 32 contexts (LRU eviction, configurable via `spring.test.context.cache.maxSize`). ## What the cache key is The key is a `MergedContextConfiguration` value object — effectively the **fingerprint of everything that could alter the resulting context**, including: - Configuration classes / XML locations (`@ContextConfiguration`, `@SpringBootTest` config). - **Active profiles** (`@ActiveProfiles`) — as an order-independent set. - **Property sources**: `@TestPropertySource` `locations` **and** inlined `properties`. - `ContextInitializer`s, `ContextCustomizer`s (e.g. `@MockBean`/`@MockitoBean` definitions, `@DynamicPropertySource` registrations), the `ContextLoader`, parent context, etc. Two test classes reuse a cached context **iff their entire `MergedContextConfiguration` is equal**. Any divergence — a different profile, a different inlined property string, an extra `@MockBean` — yields a different key and a **freshly built** context. ## The performance trap Because `@ActiveProfiles` and `@TestPropertySource` are part of the key, seemingly harmless variety fragments the cache: - `ClassA` uses `@ActiveProfiles("test")`; `ClassB` uses `@ActiveProfiles({"test","fast"})` → two contexts. - `ClassC` inlines `properties="x=1"`; `ClassD` inlines `properties="x=2"` → two contexts. - Ten test classes each with their own tiny inlined override → up to ten contexts, each a full startup. Each new context beyond a shared one adds seconds (or more) and can exceed the cache `maxSize`, causing **eviction and re-loading** later — thrashing. On large suites this dominates wall-clock time. ## Design guidance (the principal's answer) - **Converge on a few canonical setups.** Prefer a shared base test class (or meta-annotation) carrying `@ActiveProfiles` + `@TestPropertySource` so many tests share one key → one context. - **Move overrides into files.** A shared `test.properties` referenced by `locations` keeps the key identical across classes; inlining differing strings does not. - **Reserve per-class inlined `properties`** for the rare test that genuinely needs a unique value — accept it will get its own context. - **Watch other key contributors** too: `@MockBean`/`@MockitoBean` and `@DynamicPropertySource` also fork the cache; group tests that share the same mocks. - **Measure.** Enable `logging.level.org.springframework.test.context.cache=DEBUG` to see cache hit/miss/size, or watch startup counts. ## @ActiveProfiles vs @TestPropertySource in the key Both matter, but note the subtlety: `@ActiveProfiles` is compared as a **set** (order doesn't matter), while inlined `properties` are compared by content. Neither is 'free' — both partition the cache. ## Gotchas - Adding `@TestPropertySource(properties="...")` to fix one test can silently spawn a whole extra context for that class. - `@DirtiesContext` forcibly evicts a context — legitimate when a test mutates shared state, but it defeats caching, so use sparingly. - The cache is **JVM-wide** per test run, keyed independent of test order.
- How can you observe how many contexts your suite is building?Set logging.level.org.springframework.test.context.cache=DEBUG to log cache hits/misses/size, and tune spring.test.context.cache.maxSize (default 32) if you legitimately need more.
- Besides profiles and properties, name another thing that forks the context cache.@MockBean/@MockitoBean definitions, @DynamicPropertySource, ContextCustomizers/Initializers, differing config classes, and @DirtiesContext (which evicts) all change or invalidate the cache key.
saying these in an interview costs you the question
- Assuming all @SpringBootTest classes share one context regardless of profiles/properties
- Thinking inlined properties are 'free' and don't affect caching
- Overusing @DirtiesContext, unaware it defeats the cache