Introduce Spring property to disable context pausing for tests

Spring Framework 7.0 introduced support for pausing inactive
application contexts between test classes and restarting them once they
are needed again. If pausing and restarting are fast, this feature does
not have a negative impact on test suites.

However, if the pausing or restarting of certain Lifecycle components
in the application context is slow, that can have a negative impact on
the duration of the overall test suite.

In gh-36044, we hope to find a way to avoid unnecessarily pausing an
application context after a test class if the same context is used by
the next test class that is run. That should help reduce the risk of a
negative impact caused by the pause/restart feature; however, for
certain scenarios that may not be enough. In light of that, this commit
introduces a mechanism for completely disabling the pausing feature via
a Spring property or JVM system property, as follows.

-Dspring.test.context.cache.pause=never

See gh-35168
See gh-36044
Closes gh-36117
This commit is contained in:
Sam Brannen
2026-01-11 11:42:14 +01:00
parent d3a385d222
commit 9711db787e
9 changed files with 487 additions and 45 deletions
@@ -0,0 +1,201 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.cache;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextCustomizerFactories;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestContextTestUtils;
import org.springframework.test.context.cache.ContextCache.PauseMode;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.context.cache.ContextCache.DEFAULT_MAX_CONTEXT_CACHE_SIZE;
import static org.springframework.test.context.cache.ContextCacheTestUtils.assertContextCacheStatistics;
/**
* Integration tests for verifying proper behavior of the {@link ContextCache} in
* conjunction with {@link PauseMode}.
*
* @author Sam Brannen
* @since 7.0.3
* @see ContextCacheTests
* @see UnusedContextsIntegrationTests
*/
class ContextCachePauseModeTests {
private ContextCache contextCache;
@BeforeEach
@AfterEach
void clearApplicationEvents() {
EventTracker.events.clear();
}
@Test
void topLevelTestClassesWithPauseModeAlways() {
this.contextCache = new DefaultContextCache(DEFAULT_MAX_CONTEXT_CACHE_SIZE, PauseMode.ALWAYS);
loadCtxAndAssertStats(TestCase1A.class, 1, 1, 0, 1);
assertThat(EventTracker.events).containsExactly("ContextRefreshed:TestCase1A", "ContextPaused:TestCase1A");
clearApplicationEvents();
loadCtxAndAssertStats(TestCase1A.class, 1, 1, 1, 1);
assertThat(EventTracker.events).containsExactly("ContextRestarted:TestCase1A", "ContextPaused:TestCase1A");
clearApplicationEvents();
loadCtxAndAssertStats(TestCase1B.class, 1, 1, 2, 1);
assertThat(EventTracker.events).containsExactly("ContextRestarted:TestCase1A", "ContextPaused:TestCase1A");
clearApplicationEvents();
loadCtxAndAssertStats(TestCase1A.class, 1, 1, 3, 1);
assertThat(EventTracker.events).containsExactly("ContextRestarted:TestCase1A", "ContextPaused:TestCase1A");
clearApplicationEvents();
loadCtxAndAssertStats(TestCase2.class, 2, 1, 3, 2);
assertThat(EventTracker.events).containsExactly("ContextRefreshed:TestCase2", "ContextPaused:TestCase2");
clearApplicationEvents();
loadCtxAndAssertStats(TestCase1B.class, 2, 1, 4, 2);
assertThat(EventTracker.events).containsExactly("ContextRestarted:TestCase1A", "ContextPaused:TestCase1A");
clearApplicationEvents();
loadCtxAndAssertStats(TestCase1A.class, 2, 1, 5, 2);
assertThat(EventTracker.events).containsExactly("ContextRestarted:TestCase1A", "ContextPaused:TestCase1A");
clearApplicationEvents();
loadCtxAndAssertStats(TestCase2.class, 2, 1, 6, 2);
assertThat(EventTracker.events).containsExactly("ContextRestarted:TestCase2", "ContextPaused:TestCase2");
clearApplicationEvents();
loadCtxAndAssertStats(TestCase2.class, 2, 1, 7, 2);
assertThat(EventTracker.events).containsExactly("ContextRestarted:TestCase2", "ContextPaused:TestCase2");
clearApplicationEvents();
markContextDirty(TestCase2.class);
assertThat(EventTracker.events).containsExactly("ContextClosed:TestCase2");
clearApplicationEvents();
loadCtxAndAssertStats(TestCase2.class, 2, 1, 7, 3);
assertThat(EventTracker.events).containsExactly("ContextRefreshed:TestCase2", "ContextPaused:TestCase2");
clearApplicationEvents();
}
@Test
void topLevelTestClassesWithPauseModeNever() {
this.contextCache = new DefaultContextCache(DEFAULT_MAX_CONTEXT_CACHE_SIZE, PauseMode.NEVER);
loadCtxAndAssertStats(TestCase1A.class, 1, 1, 0, 1);
assertThat(EventTracker.events).containsExactly("ContextRefreshed:TestCase1A");
clearApplicationEvents();
loadCtxAndAssertStats(TestCase1A.class, 1, 1, 1, 1);
assertThat(EventTracker.events).isEmpty();
clearApplicationEvents();
loadCtxAndAssertStats(TestCase1B.class, 1, 1, 2, 1);
assertThat(EventTracker.events).isEmpty();
clearApplicationEvents();
loadCtxAndAssertStats(TestCase1A.class, 1, 1, 3, 1);
assertThat(EventTracker.events).isEmpty();
clearApplicationEvents();
loadCtxAndAssertStats(TestCase2.class, 2, 1, 3, 2);
assertThat(EventTracker.events).containsExactly("ContextRefreshed:TestCase2");
clearApplicationEvents();
loadCtxAndAssertStats(TestCase1B.class, 2, 1, 4, 2);
assertThat(EventTracker.events).isEmpty();
clearApplicationEvents();
loadCtxAndAssertStats(TestCase1A.class, 2, 1, 5, 2);
assertThat(EventTracker.events).isEmpty();
clearApplicationEvents();
loadCtxAndAssertStats(TestCase2.class, 2, 1, 6, 2);
assertThat(EventTracker.events).isEmpty();
clearApplicationEvents();
loadCtxAndAssertStats(TestCase2.class, 2, 1, 7, 2);
assertThat(EventTracker.events).isEmpty();
clearApplicationEvents();
markContextDirty(TestCase2.class);
assertThat(EventTracker.events).containsExactly("ContextClosed:TestCase2");
clearApplicationEvents();
loadCtxAndAssertStats(TestCase2.class, 2, 1, 7, 3);
assertThat(EventTracker.events).containsExactly("ContextRefreshed:TestCase2");
clearApplicationEvents();
}
private void loadCtxAndAssertStats(Class<?> testClass, int expectedSize, int expectedActiveContextsCount,
int expectedHitCount, int expectedMissCount) {
TestContext testContext = TestContextTestUtils.buildTestContext(testClass, contextCache);
ApplicationContext context = testContext.getApplicationContext();
assertThat(context).isNotNull();
assertContextCacheStatistics(contextCache, testClass.getName(), expectedSize, expectedActiveContextsCount,
expectedHitCount, expectedMissCount);
testContext.markApplicationContextUnused();
assertThat(contextCache.getContextUsageCount())
.as("active contexts in cache (%s)", testClass.getSimpleName()).isZero();
}
private void markContextDirty(Class<?> testClass) {
TestContext testContext = TestContextTestUtils.buildTestContext(testClass, contextCache);
testContext.markApplicationContextDirty(HierarchyMode.EXHAUSTIVE);
}
@Configuration
@Import(EventTracker.class)
static class Config1 {
}
@Configuration
@Import(EventTracker.class)
static class Config2 {
}
@ContextConfiguration(classes = Config1.class)
@ContextCustomizerFactories(DisplayNameCustomizerFactory.class)
private abstract static class AbstractTestCase1 {
}
private static class TestCase1A extends AbstractTestCase1 {
}
private static class TestCase1B extends AbstractTestCase1 {
}
@ContextConfiguration(classes = Config2.class)
@ContextCustomizerFactories(DisplayNameCustomizerFactory.class)
private static class TestCase2 {
}
}
@@ -71,6 +71,8 @@ class ContextCacheTests {
assertContextCacheStatistics(contextCache, testClass.getName(), expectedSize, expectedActiveContextsCount,
expectedHitCount, expectedMissCount);
testContext.markApplicationContextUnused();
assertThat(contextCache.getContextUsageCount())
.as("active contexts in cache (%s)", testClass.getSimpleName()).isZero();
}
@@ -18,14 +18,21 @@ package org.springframework.test.context.cache;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.FieldSource;
import org.springframework.core.SpringProperties;
import org.springframework.test.context.cache.ContextCache.PauseMode;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.springframework.test.context.cache.ContextCache.CONTEXT_CACHE_PAUSE_PROPERTY_NAME;
import static org.springframework.test.context.cache.ContextCache.DEFAULT_MAX_CONTEXT_CACHE_SIZE;
import static org.springframework.test.context.cache.ContextCache.MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME;
import static org.springframework.test.context.cache.ContextCacheUtils.retrieveMaxCacheSize;
import static org.springframework.test.context.cache.ContextCacheUtils.retrievePauseMode;
/**
* Tests for {@link ContextCacheUtils}.
@@ -35,56 +42,141 @@ import static org.springframework.test.context.cache.ContextCacheUtils.retrieveM
*/
class ContextCacheUtilsTests {
@BeforeEach
@AfterEach
void clearProperties() {
System.clearProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME);
SpringProperties.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, null);
@Nested
class MaxCacheSizeTests {
@BeforeEach
@AfterEach
void clearProperties() {
System.clearProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME);
SpringProperties.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, null);
}
@Test
void retrieveMaxCacheSizeFromDefault() {
assertDefaultValue();
}
@Test
void retrieveMaxCacheSizeFromBogusSystemProperty() {
System.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "bogus");
assertDefaultValue();
}
@Test
void retrieveMaxCacheSizeFromBogusSpringProperty() {
SpringProperties.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "bogus");
assertDefaultValue();
}
@Test
void retrieveMaxCacheSizeFromDecimalSpringProperty() {
SpringProperties.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "3.14");
assertDefaultValue();
}
@Test
void retrieveMaxCacheSizeFromSystemProperty() {
System.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "42");
assertThat(retrieveMaxCacheSize()).isEqualTo(42);
}
@Test
void retrieveMaxCacheSizeFromSystemPropertyContainingWhitespace() {
System.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "42\t");
assertThat(retrieveMaxCacheSize()).isEqualTo(42);
}
@Test
void retrieveMaxCacheSizeFromSpringProperty() {
SpringProperties.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "99");
assertThat(retrieveMaxCacheSize()).isEqualTo(99);
}
private static void assertDefaultValue() {
assertThat(retrieveMaxCacheSize()).isEqualTo(DEFAULT_MAX_CONTEXT_CACHE_SIZE);
}
}
@Test
void retrieveMaxCacheSizeFromDefault() {
assertDefaultValue();
}
/**
* Tests for {@link PauseMode} support.
* @since 7.0.3
*/
@Nested
class PauseModeTests {
@Test
void retrieveMaxCacheSizeFromBogusSystemProperty() {
System.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "bogus");
assertDefaultValue();
}
static final String[] ALWAYS_VALUES = {
"always",
"Always",
"ALWAYS",
"\talways\u000B"
};
@Test
void retrieveMaxCacheSizeFromBogusSpringProperty() {
SpringProperties.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "bogus");
assertDefaultValue();
}
static final String[] NEVER_VALUES = {
"never",
"Never",
"NEVER",
"\tnever\u000B"
};
@Test
void retrieveMaxCacheSizeFromDecimalSpringProperty() {
SpringProperties.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "3.14");
assertDefaultValue();
}
@Test
void retrieveMaxCacheSizeFromSystemProperty() {
System.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "42");
assertThat(retrieveMaxCacheSize()).isEqualTo(42);
}
@BeforeEach
@AfterEach
void clearProperties() {
System.clearProperty(CONTEXT_CACHE_PAUSE_PROPERTY_NAME);
SpringProperties.setProperty(CONTEXT_CACHE_PAUSE_PROPERTY_NAME, null);
}
@Test
void retrieveMaxCacheSizeFromSystemPropertyContainingWhitespace() {
System.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "42\t");
assertThat(retrieveMaxCacheSize()).isEqualTo(42);
}
@Test
void retrievePauseModeFromDefault() {
assertThat(retrievePauseMode()).isEqualTo(PauseMode.ALWAYS);
}
@Test
void retrieveMaxCacheSizeFromSpringProperty() {
SpringProperties.setProperty(MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME, "99");
assertThat(retrieveMaxCacheSize()).isEqualTo(99);
}
@Test
void retrievePauseModeFromBogusSystemProperty() {
System.setProperty(CONTEXT_CACHE_PAUSE_PROPERTY_NAME, "bogus");
asssertUnsupportedValue();
}
private static void assertDefaultValue() {
assertThat(retrieveMaxCacheSize()).isEqualTo(DEFAULT_MAX_CONTEXT_CACHE_SIZE);
@Test
void retrievePauseModeFromBogusSpringProperty() {
SpringProperties.setProperty(CONTEXT_CACHE_PAUSE_PROPERTY_NAME, "bogus");
asssertUnsupportedValue();
}
@ParameterizedTest
@FieldSource("ALWAYS_VALUES")
void retrievePauseModeFromSystemPropertyWithValueAlways(String value) {
System.setProperty(CONTEXT_CACHE_PAUSE_PROPERTY_NAME, value);
assertThat(retrievePauseMode()).isEqualTo(PauseMode.ALWAYS);
}
@ParameterizedTest
@FieldSource("NEVER_VALUES")
void retrievePauseModeFromSystemPropertyWithValueNever(String value) {
System.setProperty(CONTEXT_CACHE_PAUSE_PROPERTY_NAME, value);
assertThat(retrievePauseMode()).isEqualTo(PauseMode.NEVER);
}
@ParameterizedTest
@FieldSource("ALWAYS_VALUES")
void retrievePauseModeFromSpringPropertyWithValueAlways(String value) {
SpringProperties.setProperty(CONTEXT_CACHE_PAUSE_PROPERTY_NAME, value);
assertThat(retrievePauseMode()).isEqualTo(PauseMode.ALWAYS);
}
@ParameterizedTest
@FieldSource("NEVER_VALUES")
void retrievePauseModeFromSpringPropertyWithValueNever(String value) {
SpringProperties.setProperty(CONTEXT_CACHE_PAUSE_PROPERTY_NAME, value);
assertThat(retrievePauseMode()).isEqualTo(PauseMode.NEVER);
}
private void asssertUnsupportedValue() {
assertThatIllegalArgumentException()
.isThrownBy(ContextCacheUtils::retrievePauseMode)
.withMessage("Unsupported value 'bogus' for property 'spring.test.context.cache.pause'");
}
}
}