How Spring context caching decides to reuse
The Spring TestContext framework builds an application context for the first test class that needs it and stores it in a static cache inside the JVM. The cache key is the merged configuration: the configuration classes, active profiles, property sources, context initialisers and customisers, and the parent context if there is one. A later class with an identical key reuses the context and starts in milliseconds. A class whose key differs in any way gets a new context.
Each of these produces a different key: a @MockitoBean or @MockBean field that no other class declares, a different @TestPropertySource or properties on @SpringBootTest, a different @ActiveProfiles, an extra @Import, a different webEnvironment, or a @DynamicPropertySource method declared on the class itself rather than in a shared base. None of them looks expensive at the point of use, which is how a suite drifts to dozens of distinct contexts without anyone deciding it should.
On versions: @MockBean was deprecated in Spring Boot 3.4 in favour of @MockitoBean from Spring Framework 6.2, and removed in Boot 4.0. The replacement does not change the caching behaviour. A bean override is still part of the key.
Count the contexts before changing anything
Context caching reports on itself, so Spring will tell you how many contexts it built. Set the logger org.springframework.test.context.cache to DEBUG and the framework logs cache statistics as tests run, including the current size, the maximum, and hit and miss counts. The miss count at the end of the run is the number of contexts started.
# In a test-only properties file or a logback-test.xml logger; a file named
# src/test/resources/application.properties would shadow the main one on the classpath
logging.level.org.springframework.test.context.cache=DEBUG
Turn that into a time budget. Assume 400 classes in 1,200 seconds and a typical context taking 8 seconds to start. If the log shows 120 misses, then 960 seconds, about four-fifths of the run, is Spring starting up. If it shows 6 misses, contexts are not the problem and you should look at the slowest individual tests instead. The figures are an assumption to show the arithmetic; your own log supplies the real ones, and the ratio is what decides the fix.
The cache bound turns drift into thrashing
The context cache is bounded, at 32 entries by default, and it evicts the least recently used context when full. The limit can be changed with the spring.test.context.cache.maxSize property. Below the limit, each distinct configuration costs one startup. Above it, the cost stops being linear in the number of configurations, because a configuration evicted early is rebuilt when a later class needs it again. Test ordering then decides how often that happens, so the suite's duration changes when someone adds an unrelated class.
Raising maxSize is tempting and usually wrong. Every cached context stays alive, with its connection pool, scheduled tasks, embedded server and caches. Thirty live contexts each holding ten database connections can exhaust the test database's connection limit. The fix is fewer configurations, not a bigger cache.
The cache only lives as long as the JVM
Context caching is per JVM, held in a static field. A build that forks a new JVM per test class, through Surefire's reuseForks=false or Gradle's forkEvery = 1, starts every class with an empty cache, and the hit count is zero no matter how disciplined the annotations are. Parallel forks multiply the contexts, since each fork has its own cache. Check the build configuration before the test code, because a single setting can undo everything else.
@DirtiesContext charges every class that runs later
@DirtiesContext closes and evicts the context after the class or method. To the author it looks like hygiene. To the suite it means the next class with the same configuration rebuilds a context that was working. A method-level @DirtiesContext on a frequently shared configuration can cost more than every mock bean combined. The usual reason for adding it is state that leaked into a singleton or the database, and the better fix is to reset that state in the test.
Converge, then slice
The largest win is usually converging configurations, so that context caching finally gets hits. Declare the shared mock beans, Testcontainers wiring and properties once, in an abstract base class or a meta-annotation, so most integration tests resolve to the same key. A mock that only one class needs is often a sign that class should not be a @SpringBootTest at all.
The second win is test slices. @WebMvcTest starts only the MVC layer for the controller under test, @DataJpaTest starts only JPA and repositories, and @JsonTest starts only serialisation. They are smaller contexts, and they share well among themselves. A test that checks a calculation through the full application context belongs in a plain JUnit test with no Spring at all. The broader question of where that boundary sits is covered in unit versus integration boundaries; this page is about finding the contexts.
Answering with fixes instead of a count
The adequate answer lists the usual remedies: slices, fewer mocks, no @DirtiesContext, parallel execution. The strong answer starts by counting misses. With that number in hand, the candidate can show whether the suite is under the cache limit or thrashing above it, and can name the build setting that might be discarding the cache altogether. Parallel execution is the remedy to distrust most, because it divides the wall-clock time while multiplying the contexts. A suite with 120 distinct configurations spread across four forks can start Spring 480 times before a single eviction, because each fork may need every configuration and each fork's cache still holds only 32.
Count the cache misses first. A slow @SpringBootTest suite is almost always paying for configurations nobody meant to make distinct, and the fix is fewer keys, not more parallelism.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).