skip to content

How do @ActiveProfiles and @TestPropertySource affect the TestContext context cache, and why can careless use slow a large test suite?

level: principalimportance: should knowfreq 30%

answer

  1. MergedContextConfiguration = cache key
  2. Profiles (as set) + property sources both in the key
  3. Distinct combo => new full startup
  4. @MockBean / @DynamicPropertySource also fork cache
  5. Share via base class / files, not per-class inline

basics

~20 s

The 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 s

Spring 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
kotlin
// 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 RateEdgeCaseIT

go deeper

for a junior

Know contexts are cached to speed tests.

for a middle

Know profiles and property overrides change which context is reused.

for a senior

Explain MergedContextConfiguration and the main key contributors.

for a principal

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

context