Pause unused application contexts in the TestContext framework

Since the introduction of the Spring TestContext Framework in 2007,
application contexts have always been stored in the context cache in a
"running" state. However, leaving a context running means that
components in the context may continue to run in the background. For
example, JMS listeners may continue to consume messages from a queue;
scheduled tasks may continue to perform active work, etc.; and this can
lead to issues within a test suite.

To address such issues, this commit introduces built-in support for
pausing application contexts when they are not in use and restarting
them if they are needed again.

Specifically, the TestContextManager now marks a test's application
context as "unused" after execution of the test class has ended, and
the underlying ContextCache then "stops" the application context if no
other test class is currently using the context. When a
TestExecutionListener later attempts to obtain a paused application
context -- for example, for a subsequent test class that shares the
same application context -- the ContextCache ensures that context is
restarted before returning it.

See https://github.com/spring-projects/spring-boot/issues/28312
See gh-35171
Closes gh-35168
This commit is contained in:
Sam Brannen
2025-07-14 18:29:02 +02:00
parent f3757cedb9
commit dbe89abd7b
23 changed files with 1031 additions and 160 deletions
@@ -64,7 +64,7 @@ class ClassLevelDirtiesContextTestNGTests {
// for example, via JUnit's @Suite.
cacheHits.set(0);
cacheMisses.set(0);
assertContextCacheStatistics("BeforeClass", 0, cacheHits.get(), cacheMisses.get());
assertContextCacheStatistics("BeforeClass", 0, 0, cacheHits.get(), cacheMisses.get());
}
@Test
@@ -74,63 +74,63 @@ class ClassLevelDirtiesContextTestNGTests {
runTestClassAndAssertStats(ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with clean test method and default class mode",
0, cacheHits.incrementAndGet(), cacheMisses.get());
0, 0, cacheHits.incrementAndGet(), cacheMisses.get());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase.class, 1);
assertContextCacheStatistics(
"after inherited class-level @DirtiesContext with clean test method and default class mode", 0,
cacheHits.incrementAndGet(), cacheMisses.get());
0, cacheHits.incrementAndGet(), cacheMisses.get());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with clean test method and AFTER_CLASS mode",
0, cacheHits.incrementAndGet(), cacheMisses.get());
0, 0, cacheHits.incrementAndGet(), cacheMisses.get());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase.class, 1);
assertContextCacheStatistics(
"after inherited class-level @DirtiesContext with clean test method and AFTER_CLASS mode", 0,
cacheHits.incrementAndGet(), cacheMisses.get());
0, cacheHits.incrementAndGet(), cacheMisses.get());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase.class, 3);
assertContextCacheStatistics(
"after class-level @DirtiesContext with clean test method and AFTER_EACH_TEST_METHOD mode", 0,
cacheHits.incrementAndGet(), cacheMisses.addAndGet(2));
0, cacheHits.incrementAndGet(), cacheMisses.addAndGet(2));
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase.class, 3);
assertContextCacheStatistics(
"after inherited class-level @DirtiesContext with clean test method and AFTER_EACH_TEST_METHOD mode", 0,
cacheHits.incrementAndGet(), cacheMisses.addAndGet(2));
0, cacheHits.incrementAndGet(), cacheMisses.addAndGet(2));
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0,
cacheHits.incrementAndGet(), cacheMisses.get());
0, cacheHits.incrementAndGet(), cacheMisses.get());
runTestClassAndAssertStats(ClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0, cacheHits.get(),
assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0, 0, cacheHits.get(),
cacheMisses.incrementAndGet());
runTestClassAndAssertStats(ClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0, cacheHits.get(),
assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0, 0, cacheHits.get(),
cacheMisses.incrementAndGet());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after inherited class-level @DirtiesContext with dirty test method", 0,
cacheHits.incrementAndGet(), cacheMisses.get());
0, cacheHits.incrementAndGet(), cacheMisses.get());
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after inherited class-level @DirtiesContext with dirty test method", 0,
cacheHits.get(), cacheMisses.incrementAndGet());
0, cacheHits.get(), cacheMisses.incrementAndGet());
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after inherited class-level @DirtiesContext with dirty test method", 0,
cacheHits.get(), cacheMisses.incrementAndGet());
0, cacheHits.get(), cacheMisses.incrementAndGet());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with clean test method and AFTER_CLASS mode",
0, cacheHits.incrementAndGet(), cacheMisses.get());
0, 0, cacheHits.incrementAndGet(), cacheMisses.get());
}
private void runTestClassAndAssertStats(Class<?> testClass, int expectedTestCount) {
@@ -152,12 +152,12 @@ class ClassLevelDirtiesContextTestNGTests {
private void assertBehaviorForCleanTestCase() {
runTestClassAndAssertStats(CleanTestCase.class, 1);
assertContextCacheStatistics("after clean test class", 1, cacheHits.get(), cacheMisses.incrementAndGet());
assertContextCacheStatistics("after clean test class", 1, 0, cacheHits.get(), cacheMisses.incrementAndGet());
}
@AfterAll
static void verifyFinalCacheState() {
assertContextCacheStatistics("AfterClass", 0, cacheHits.get(), cacheMisses.get());
assertContextCacheStatistics("AfterClass", 0, 0, cacheHits.get(), cacheMisses.get());
}
@@ -62,81 +62,81 @@ class ClassLevelDirtiesContextTests {
// for example, via JUnit's @Suite.
cacheHits.set(0);
cacheMisses.set(0);
assertContextCacheStatistics("BeforeClass", 0, cacheHits.get(), cacheMisses.get());
assertContextCacheStatistics("BeforeClass", 0, 0, cacheHits.get(), cacheMisses.get());
}
@Test
void verifyDirtiesContextBehavior() throws Exception {
void verifyDirtiesContextBehavior() {
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with clean test method and default class mode",
0, cacheHits.incrementAndGet(), cacheMisses.get());
0, 0, cacheHits.incrementAndGet(), cacheMisses.get());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase.class, 1);
assertContextCacheStatistics(
"after inherited class-level @DirtiesContext with clean test method and default class mode", 0,
cacheHits.incrementAndGet(), cacheMisses.get());
0, cacheHits.incrementAndGet(), cacheMisses.get());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with clean test method and AFTER_CLASS mode",
0, cacheHits.incrementAndGet(), cacheMisses.get());
0, 0, cacheHits.incrementAndGet(), cacheMisses.get());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase.class, 1);
assertContextCacheStatistics(
"after inherited class-level @DirtiesContext with clean test method and AFTER_CLASS mode", 0,
cacheHits.incrementAndGet(), cacheMisses.get());
0, cacheHits.incrementAndGet(), cacheMisses.get());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase.class, 3);
assertContextCacheStatistics(
"after class-level @DirtiesContext with clean test method and AFTER_EACH_TEST_METHOD mode", 0,
cacheHits.incrementAndGet(), cacheMisses.addAndGet(2));
0, cacheHits.incrementAndGet(), cacheMisses.addAndGet(2));
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase.class, 3);
assertContextCacheStatistics(
"after inherited class-level @DirtiesContext with clean test method and AFTER_EACH_TEST_METHOD mode", 0,
cacheHits.incrementAndGet(), cacheMisses.addAndGet(2));
0, cacheHits.incrementAndGet(), cacheMisses.addAndGet(2));
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0,
cacheHits.incrementAndGet(), cacheMisses.get());
0, cacheHits.incrementAndGet(), cacheMisses.get());
runTestClassAndAssertStats(ClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0, cacheHits.get(),
assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0, 0, cacheHits.get(),
cacheMisses.incrementAndGet());
runTestClassAndAssertStats(ClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0, cacheHits.get(),
assertContextCacheStatistics("after class-level @DirtiesContext with dirty test method", 0, 0, cacheHits.get(),
cacheMisses.incrementAndGet());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after inherited class-level @DirtiesContext with dirty test method", 0,
cacheHits.incrementAndGet(), cacheMisses.get());
0, cacheHits.incrementAndGet(), cacheMisses.get());
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after inherited class-level @DirtiesContext with dirty test method", 0,
cacheHits.get(), cacheMisses.incrementAndGet());
0, cacheHits.get(), cacheMisses.incrementAndGet());
runTestClassAndAssertStats(InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase.class, 1);
assertContextCacheStatistics("after inherited class-level @DirtiesContext with dirty test method", 0,
cacheHits.get(), cacheMisses.incrementAndGet());
0, cacheHits.get(), cacheMisses.incrementAndGet());
assertBehaviorForCleanTestCase();
runTestClassAndAssertStats(ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with clean test method and AFTER_CLASS mode",
0, cacheHits.incrementAndGet(), cacheMisses.get());
0, 0, cacheHits.incrementAndGet(), cacheMisses.get());
}
private void assertBehaviorForCleanTestCase() throws Exception {
private void assertBehaviorForCleanTestCase() {
runTestClassAndAssertStats(CleanTestCase.class, 1);
assertContextCacheStatistics("after clean test class", 1, cacheHits.get(), cacheMisses.incrementAndGet());
assertContextCacheStatistics("after clean test class", 1, 0, cacheHits.get(), cacheMisses.incrementAndGet());
}
private void runTestClassAndAssertStats(Class<?> testClass, int expectedTestCount) throws Exception {
private void runTestClassAndAssertStats(Class<?> testClass, int expectedTestCount) {
EngineTestKit.engine("junit-jupiter")
.selectors(selectClass(testClass))
.execute()
@@ -151,7 +151,7 @@ class ClassLevelDirtiesContextTests {
@AfterAll
static void verifyFinalCacheState() {
assertContextCacheStatistics("AfterClass", 0, cacheHits.get(), cacheMisses.get());
assertContextCacheStatistics("AfterClass", 0, 0, cacheHits.get(), cacheMisses.get());
}
@@ -44,7 +44,17 @@ public class ContextCacheTestUtils {
* @param expectedMissCount the expected miss count
*/
public static void assertContextCacheStatistics(int expectedSize, int expectedHitCount, int expectedMissCount) {
assertContextCacheStatistics(null, expectedSize, expectedHitCount, expectedMissCount);
ContextCache contextCache = DefaultCacheAwareContextLoaderDelegate.defaultContextCache;
String context = (StringUtils.hasText((String) null) ? " (" + null + ")" : "");
assertSoftly(softly -> {
softly.assertThat(contextCache.size())
.as("contexts in cache" + context).isEqualTo(expectedSize);
softly.assertThat(contextCache.getHitCount())
.as("cache hits" + context).isEqualTo(expectedHitCount);
softly.assertThat(contextCache.getMissCount())
.as("cache misses" + context).isEqualTo(expectedMissCount);
});
}
/**
@@ -52,13 +62,16 @@ public class ContextCacheTestUtils {
*
* @param usageScenario the scenario in which the statistics are used
* @param expectedSize the expected number of contexts in the cache
* @param expectedContextUsageCount the expected number of actively running
* contexts in the cache
* @param expectedHitCount the expected hit count
* @param expectedMissCount the expected miss count
*/
public static void assertContextCacheStatistics(String usageScenario, int expectedSize, int expectedHitCount,
int expectedMissCount) {
public static void assertContextCacheStatistics(String usageScenario, int expectedSize,
int expectedContextUsageCount, int expectedHitCount, int expectedMissCount) {
assertContextCacheStatistics(DefaultCacheAwareContextLoaderDelegate.defaultContextCache, usageScenario,
expectedSize, expectedHitCount, expectedMissCount);
expectedSize, expectedContextUsageCount, expectedHitCount, expectedMissCount);
}
/**
@@ -67,18 +80,25 @@ public class ContextCacheTestUtils {
* @param contextCache the cache to assert against
* @param usageScenario the scenario in which the statistics are used
* @param expectedSize the expected number of contexts in the cache
* @param expectedContextUsageCount the expected number of actively running
* contexts in the cache
* @param expectedHitCount the expected hit count
* @param expectedMissCount the expected miss count
*/
public static void assertContextCacheStatistics(ContextCache contextCache, String usageScenario,
int expectedSize, int expectedHitCount, int expectedMissCount) {
int expectedSize, int expectedContextUsageCount, int expectedHitCount, int expectedMissCount) {
String context = (StringUtils.hasText(usageScenario) ? " (" + usageScenario + ")" : "");
assertSoftly(softly -> {
softly.assertThat(contextCache.size()).as("contexts in cache" + context).isEqualTo(expectedSize);
softly.assertThat(contextCache.getHitCount()).as("cache hits" + context).isEqualTo(expectedHitCount);
softly.assertThat(contextCache.getMissCount()).as("cache misses" + context).isEqualTo(expectedMissCount);
softly.assertThat(contextCache.size())
.as("contexts in cache" + context).isEqualTo(expectedSize);
softly.assertThat(contextCache.getContextUsageCount())
.as("active contexts in cache" + context).isEqualTo(expectedContextUsageCount);
softly.assertThat(contextCache.getHitCount())
.as("cache hits" + context).isEqualTo(expectedHitCount);
softly.assertThat(contextCache.getMissCount())
.as("cache misses" + context).isEqualTo(expectedMissCount);
});
}
@@ -19,13 +19,11 @@ package org.springframework.test.context.cache;
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.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ActiveProfilesResolver;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextHierarchy;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestContextTestUtils;
@@ -52,7 +50,7 @@ class ContextCacheTests {
@BeforeEach
void initialCacheState() {
assertContextCacheStatistics(contextCache, "initial state", 0, 0, 0);
assertContextCacheStatistics(contextCache, "initial state", 0, 0, 0, 0);
assertParentContextCount(0);
}
@@ -64,38 +62,39 @@ class ContextCacheTests {
return (MergedContextConfiguration) ReflectionTestUtils.getField(testContext, "mergedConfig");
}
private ApplicationContext loadContext(Class<?> testClass) {
private void loadCtxAndAssertStats(Class<?> testClass, int expectedSize, int expectedActiveContextsCount,
int expectedHitCount, int expectedMissCount) {
TestContext testContext = TestContextTestUtils.buildTestContext(testClass, contextCache);
return testContext.getApplicationContext();
assertThat(testContext.getApplicationContext()).isNotNull();
assertContextCacheStatistics(contextCache, testClass.getName(), expectedSize, expectedActiveContextsCount,
expectedHitCount, expectedMissCount);
testContext.markApplicationContextUnused();
}
private void loadCtxAndAssertStats(Class<?> testClass, int expectedSize, int expectedHitCount, int expectedMissCount) {
assertThat(loadContext(testClass)).isNotNull();
assertContextCacheStatistics(contextCache, testClass.getName(), expectedSize, expectedHitCount,
expectedMissCount);
}
@Test
void verifyCacheKeyIsBasedOnContextLoader() {
loadCtxAndAssertStats(AnnotationConfigContextLoaderTestCase.class, 1, 0, 1);
loadCtxAndAssertStats(AnnotationConfigContextLoaderTestCase.class, 1, 1, 1);
loadCtxAndAssertStats(CustomAnnotationConfigContextLoaderTestCase.class, 2, 1, 2);
loadCtxAndAssertStats(CustomAnnotationConfigContextLoaderTestCase.class, 2, 2, 2);
loadCtxAndAssertStats(AnnotationConfigContextLoaderTestCase.class, 2, 3, 2);
loadCtxAndAssertStats(CustomAnnotationConfigContextLoaderTestCase.class, 2, 4, 2);
loadCtxAndAssertStats(AnnotationConfigContextLoaderTestCase.class, 1, 1, 0, 1);
loadCtxAndAssertStats(AnnotationConfigContextLoaderTestCase.class, 1, 1, 1, 1);
loadCtxAndAssertStats(CustomAnnotationConfigContextLoaderTestCase.class, 2, 1, 1, 2);
loadCtxAndAssertStats(CustomAnnotationConfigContextLoaderTestCase.class, 2, 1, 2, 2);
loadCtxAndAssertStats(AnnotationConfigContextLoaderTestCase.class, 2, 1, 3, 2);
loadCtxAndAssertStats(CustomAnnotationConfigContextLoaderTestCase.class, 2, 1, 4, 2);
}
@Test
void verifyCacheKeyIsBasedOnActiveProfiles() {
int size = 0, hit = 0, miss = 0;
loadCtxAndAssertStats(FooBarProfilesTestCase.class, ++size, hit, ++miss);
loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, ++hit, miss);
loadCtxAndAssertStats(FooBarProfilesTestCase.class, ++size, 1, hit, ++miss);
loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, 1, ++hit, miss);
// Profiles {foo, bar} should not hash to the same as {bar,foo}
loadCtxAndAssertStats(BarFooProfilesTestCase.class, ++size, hit, ++miss);
loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, ++hit, miss);
loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, ++hit, miss);
loadCtxAndAssertStats(BarFooProfilesTestCase.class, size, ++hit, miss);
loadCtxAndAssertStats(FooBarActiveProfilesResolverTestCase.class, size, ++hit, miss);
loadCtxAndAssertStats(BarFooProfilesTestCase.class, ++size, 1, hit, ++miss);
loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, 1, ++hit, miss);
loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, 1, ++hit, miss);
loadCtxAndAssertStats(BarFooProfilesTestCase.class, size, 1, ++hit, miss);
loadCtxAndAssertStats(FooBarActiveProfilesResolverTestCase.class, size, 1, ++hit, miss);
}
@Test
@@ -105,24 +104,24 @@ class ContextCacheTests {
int misses = 0;
// Level 1
loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel1TestCase.class, ++size, hits, ++misses);
loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel1TestCase.class, size, ++hits, misses);
loadCtxAndAssertStats(ContextHierarchyLevel1TestCase.class, ++size, 1, hits, ++misses);
loadCtxAndAssertStats(ContextHierarchyLevel1TestCase.class, size, 1, ++hits, misses);
// Level 2
loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel2TestCase.class, ++size /* L2 */, ++hits /* L1 */,
loadCtxAndAssertStats(ContextHierarchyLevel2TestCase.class, ++size /* L2 */, 2, ++hits /* L1 */,
++misses /* L2 */);
loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel2TestCase.class, size, ++hits /* L2 */, misses);
loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel2TestCase.class, size, ++hits /* L2 */, misses);
loadCtxAndAssertStats(ContextHierarchyLevel2TestCase.class, size, 2, ++hits /* L2 */, misses);
loadCtxAndAssertStats(ContextHierarchyLevel2TestCase.class, size, 2, ++hits /* L2 */, misses);
// Level 3-A
loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel3aTestCase.class, ++size /* L3A */, ++hits /* L2 */,
loadCtxAndAssertStats(ContextHierarchyLevel3a1TestCase.class, ++size /* L3A */, 3, ++hits /* L2 */,
++misses /* L3A */);
loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel3aTestCase.class, size, ++hits /* L3A */, misses);
loadCtxAndAssertStats(ContextHierarchyLevel3a1TestCase.class, size, 3, ++hits /* L3A */, misses);
// Level 3-B
loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel3bTestCase.class, ++size /* L3B */, ++hits /* L2 */,
loadCtxAndAssertStats(ContextHierarchyLevel3bTestCase.class, ++size /* L3B */, 3, ++hits /* L2 */,
++misses /* L3B */);
loadCtxAndAssertStats(ClassHierarchyContextHierarchyLevel3bTestCase.class, size, ++hits /* L3B */, misses);
loadCtxAndAssertStats(ContextHierarchyLevel3bTestCase.class, size, 3, ++hits /* L3B */, misses);
}
@Test
@@ -130,23 +129,27 @@ class ContextCacheTests {
// Load Level 3-A
TestContext testContext3a = TestContextTestUtils.buildTestContext(
ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache);
ContextHierarchyLevel3a1TestCase.class, contextCache);
testContext3a.getApplicationContext();
assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3);
assertContextCacheStatistics(contextCache, "level 3, A", 3, 3, 0, 3);
assertParentContextCount(2);
testContext3a.markApplicationContextUnused();
assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 0, 3);
// Load Level 3-B
TestContext testContext3b = TestContextTestUtils.buildTestContext(
ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache);
ContextHierarchyLevel3bTestCase.class, contextCache);
testContext3b.getApplicationContext();
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4);
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 3, 1, 4);
assertParentContextCount(2);
testContext3b.markApplicationContextUnused();
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 0, 1, 4);
// Remove Level 1
// Should also remove Levels 2, 3-A, and 3-B, leaving nothing.
contextCache.remove(getMergedContextConfiguration(testContext3a).getParent().getParent(),
HierarchyMode.CURRENT_LEVEL);
assertContextCacheStatistics(contextCache, "removed level 1", 0, 1, 4);
assertContextCacheStatistics(contextCache, "removed level 1", 0, 0, 1, 4);
assertParentContextCount(0);
}
@@ -155,23 +158,27 @@ class ContextCacheTests {
// Load Level 3-A
TestContext testContext3a = TestContextTestUtils.buildTestContext(
ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache);
ContextHierarchyLevel3a1TestCase.class, contextCache);
testContext3a.getApplicationContext();
assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3);
assertContextCacheStatistics(contextCache, "level 3, A", 3, 3, 0, 3);
assertParentContextCount(2);
testContext3a.markApplicationContextUnused();
assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 0, 3);
// Load Level 3-B
TestContext testContext3b = TestContextTestUtils.buildTestContext(
ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache);
ContextHierarchyLevel3bTestCase.class, contextCache);
testContext3b.getApplicationContext();
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4);
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 3, 1, 4);
assertParentContextCount(2);
testContext3b.markApplicationContextUnused();
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 0, 1, 4);
// Remove Level 1
// Should also remove Levels 2, 3-A, and 3-B, leaving nothing.
contextCache.remove(getMergedContextConfiguration(testContext3a).getParent().getParent(),
HierarchyMode.EXHAUSTIVE);
assertContextCacheStatistics(contextCache, "removed level 1", 0, 1, 4);
assertContextCacheStatistics(contextCache, "removed level 1", 0, 0, 1, 4);
assertParentContextCount(0);
}
@@ -180,24 +187,28 @@ class ContextCacheTests {
// Load Level 3-A
TestContext testContext3a = TestContextTestUtils.buildTestContext(
ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache);
ContextHierarchyLevel3a1TestCase.class, contextCache);
testContext3a.getApplicationContext();
assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3);
assertContextCacheStatistics(contextCache, "level 3, A", 3, 3, 0, 3);
assertParentContextCount(2);
testContext3a.markApplicationContextUnused();
assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 0, 3);
// Load Level 3-B
TestContext testContext3b = TestContextTestUtils.buildTestContext(
ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache);
ContextHierarchyLevel3bTestCase.class, contextCache);
testContext3b.getApplicationContext();
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4);
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 3, 1, 4);
assertParentContextCount(2);
testContext3b.markApplicationContextUnused();
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 0, 1, 4);
// Remove Level 2
// Should also remove Levels 3-A and 3-B, leaving only Level 1 as a context in the
// cache but also removing the Level 1 hierarchy since all children have been
// removed.
contextCache.remove(getMergedContextConfiguration(testContext3a).getParent(), HierarchyMode.CURRENT_LEVEL);
assertContextCacheStatistics(contextCache, "removed level 2", 1, 1, 4);
assertContextCacheStatistics(contextCache, "removed level 2", 1, 0, 1, 4);
assertParentContextCount(0);
}
@@ -206,22 +217,26 @@ class ContextCacheTests {
// Load Level 3-A
TestContext testContext3a = TestContextTestUtils.buildTestContext(
ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache);
ContextHierarchyLevel3a1TestCase.class, contextCache);
testContext3a.getApplicationContext();
assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3);
assertContextCacheStatistics(contextCache, "level 3, A", 3, 3, 0, 3);
assertParentContextCount(2);
testContext3a.markApplicationContextUnused();
assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 0, 3);
// Load Level 3-B
TestContext testContext3b = TestContextTestUtils.buildTestContext(
ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache);
ContextHierarchyLevel3bTestCase.class, contextCache);
testContext3b.getApplicationContext();
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4);
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 3, 1, 4);
assertParentContextCount(2);
testContext3b.markApplicationContextUnused();
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 0, 1, 4);
// Remove Level 2
// Should wipe the cache
contextCache.remove(getMergedContextConfiguration(testContext3a).getParent(), HierarchyMode.EXHAUSTIVE);
assertContextCacheStatistics(contextCache, "removed level 2", 0, 1, 4);
assertContextCacheStatistics(contextCache, "removed level 2", 0, 0, 1, 4);
assertParentContextCount(0);
}
@@ -230,27 +245,31 @@ class ContextCacheTests {
// Load Level 3-A
TestContext testContext3a = TestContextTestUtils.buildTestContext(
ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache);
ContextHierarchyLevel3a1TestCase.class, contextCache);
testContext3a.getApplicationContext();
assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3);
assertContextCacheStatistics(contextCache, "level 3, A", 3, 3, 0, 3);
assertParentContextCount(2);
testContext3a.markApplicationContextUnused();
assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 0, 3);
// Load Level 3-B
TestContext testContext3b = TestContextTestUtils.buildTestContext(
ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache);
ContextHierarchyLevel3bTestCase.class, contextCache);
testContext3b.getApplicationContext();
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4);
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 3, 1, 4);
assertParentContextCount(2);
testContext3b.markApplicationContextUnused();
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 0, 1, 4);
// Remove Level 3-A
contextCache.remove(getMergedContextConfiguration(testContext3a), HierarchyMode.CURRENT_LEVEL);
assertContextCacheStatistics(contextCache, "removed level 3-A", 3, 1, 4);
assertContextCacheStatistics(contextCache, "removed level 3-A", 3, 0, 1, 4);
assertParentContextCount(2);
// Remove Level 2
// Should also remove Level 3-B, leaving only Level 1.
contextCache.remove(getMergedContextConfiguration(testContext3b).getParent(), HierarchyMode.CURRENT_LEVEL);
assertContextCacheStatistics(contextCache, "removed level 2", 1, 1, 4);
assertContextCacheStatistics(contextCache, "removed level 2", 1, 0, 1, 4);
assertParentContextCount(0);
}
@@ -259,29 +278,33 @@ class ContextCacheTests {
// Load Level 3-A
TestContext testContext3a = TestContextTestUtils.buildTestContext(
ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache);
ContextHierarchyLevel3a1TestCase.class, contextCache);
testContext3a.getApplicationContext();
assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3);
assertContextCacheStatistics(contextCache, "level 3, A", 3, 3, 0, 3);
assertParentContextCount(2);
testContext3a.markApplicationContextUnused();
assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 0, 3);
// Load Level 3-B
TestContext testContext3b = TestContextTestUtils.buildTestContext(
ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache);
ContextHierarchyLevel3bTestCase.class, contextCache);
testContext3b.getApplicationContext();
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4);
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 3, 1, 4);
assertParentContextCount(2);
testContext3b.markApplicationContextUnused();
assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 0, 1, 4);
// Remove Level 3-A
// Should wipe the cache.
contextCache.remove(getMergedContextConfiguration(testContext3a), HierarchyMode.EXHAUSTIVE);
assertContextCacheStatistics(contextCache, "removed level 3-A", 0, 1, 4);
assertContextCacheStatistics(contextCache, "removed level 3-A", 0, 0, 1, 4);
assertParentContextCount(0);
// Remove Level 2
// Should not actually do anything since the cache was cleared in the
// previous step. So the stats should remain the same.
contextCache.remove(getMergedContextConfiguration(testContext3b).getParent(), HierarchyMode.EXHAUSTIVE);
assertContextCacheStatistics(contextCache, "removed level 2", 0, 1, 4);
assertContextCacheStatistics(contextCache, "removed level 2", 0, 0, 1, 4);
assertParentContextCount(0);
}
@@ -324,43 +347,4 @@ class ContextCacheTests {
private static class FooBarActiveProfilesResolverTestCase {
}
@ContextHierarchy({ @ContextConfiguration })
private static class ClassHierarchyContextHierarchyLevel1TestCase {
@Configuration
static class Level1Config {
}
}
@ContextHierarchy({ @ContextConfiguration })
private static class ClassHierarchyContextHierarchyLevel2TestCase extends
ClassHierarchyContextHierarchyLevel1TestCase {
@Configuration
static class Level2Config {
}
}
@ContextHierarchy({ @ContextConfiguration })
private static class ClassHierarchyContextHierarchyLevel3aTestCase extends
ClassHierarchyContextHierarchyLevel2TestCase {
@Configuration
static class Level3aConfig {
}
}
@ContextHierarchy({ @ContextConfiguration })
private static class ClassHierarchyContextHierarchyLevel3bTestCase extends
ClassHierarchyContextHierarchyLevel2TestCase {
@Configuration
static class Level3bConfig {
}
}
}
@@ -0,0 +1,44 @@
/*
* 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.Test;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ContextCustomizerFactories;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* @author Sam Brannen
* @since 7.0
*/
@SpringJUnitConfig
@ContextCustomizerFactories(DisplayNameCustomizerFactory.class)
class ContextHierarchyLevel1TestCase {
@Test
void test() {
// no-op
}
@Configuration
@Import(EventTracker.class)
static class Level1Config {
}
}
@@ -0,0 +1,34 @@
/*
* 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.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextHierarchy;
/**
* @author Sam Brannen
* @since 7.0
*/
@ContextHierarchy(@ContextConfiguration)
class ContextHierarchyLevel2TestCase extends ContextHierarchyLevel1TestCase {
@Configuration
static class Level2Config {
}
}
@@ -0,0 +1,34 @@
/*
* 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.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextHierarchy;
/**
* @author Sam Brannen
* @since 7.0
*/
@ContextHierarchy(@ContextConfiguration)
class ContextHierarchyLevel3a1TestCase extends ContextHierarchyLevel2TestCase {
@Configuration
static class Level3a1Config {
}
}
@@ -0,0 +1,28 @@
/*
* 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.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextHierarchy;
/**
* @author Sam Brannen
* @since 7.0
*/
@ContextHierarchy(@ContextConfiguration(classes = ContextHierarchyLevel3a1TestCase.Level3a1Config.class))
class ContextHierarchyLevel3a2TestCase extends ContextHierarchyLevel2TestCase {
}
@@ -0,0 +1,34 @@
/*
* 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.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextHierarchy;
/**
* @author Sam Brannen
* @since 7.0
*/
@ContextHierarchy(@ContextConfiguration)
class ContextHierarchyLevel3bTestCase extends ContextHierarchyLevel2TestCase {
@Configuration
static class Level3bConfig {
}
}
@@ -0,0 +1,42 @@
/*
* 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 java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
/**
* @author Sam Brannen
* @since 7.0
*/
class DisplayNameCustomizerFactory implements ContextCustomizerFactory {
@Override
public @Nullable ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> __) {
return (context, mergedConfig) ->
((AbstractApplicationContext) context).setDisplayName(mergedConfig.getTestClass().getSimpleName());
}
}
@@ -0,0 +1,88 @@
/*
* 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 java.util.ArrayList;
import java.util.List;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.ApplicationContextEvent;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.ContextRestartedEvent;
import org.springframework.context.event.ContextStoppedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.test.context.event.TestContextEvent;
import org.springframework.test.context.event.annotation.AfterTestClass;
import org.springframework.test.context.event.annotation.BeforeTestClass;
/**
* @author Sam Brannen
* @since 7.0
*/
@Component
class EventTracker {
static final List<String> events = new ArrayList<>();
@EventListener(ContextRefreshedEvent.class)
void contextRefreshed(ContextRefreshedEvent event) {
trackApplicationContextEvent(event);
}
@EventListener(ContextRestartedEvent.class)
void contextRestarted(ContextRestartedEvent event) {
trackApplicationContextEvent(event);
}
@EventListener(ContextStoppedEvent.class)
void contextStopped(ContextStoppedEvent event) {
trackApplicationContextEvent(event);
}
@EventListener(ContextClosedEvent.class)
void contextClosed(ContextClosedEvent event) {
trackApplicationContextEvent(event);
}
@BeforeTestClass
void beforeTestClass(TestContextEvent event) {
trackTestContextEvent(event);
}
@AfterTestClass
void afterTestClass(TestContextEvent event) {
trackTestContextEvent(event);
}
private static void trackApplicationContextEvent(ApplicationContextEvent event) {
events.add(eventName(event) + ":" + event.getSource().getDisplayName());
}
private static void trackTestContextEvent(TestContextEvent event) {
events.add(eventName(event) + ":" +
event.getSource().getTestClass().getSimpleName());
}
private static String eventName(ApplicationEvent event) {
String name = event.getClass().getSimpleName();
return name.substring(0, name.length() - "Event".length());
}
}
@@ -61,19 +61,19 @@ class SpringExtensionContextCacheTests {
static void verifyInitialCacheState() {
dirtiedApplicationContext = null;
resetContextCache();
assertContextCacheStatistics("BeforeClass", 0, 0, 0);
assertContextCacheStatistics("BeforeClass", 0, 0, 0, 0);
}
@AfterAll
static void verifyFinalCacheState() {
assertContextCacheStatistics("AfterClass", 1, 1, 2);
assertContextCacheStatistics("AfterClass", 1, 1, 1, 2);
}
@Test
@DirtiesContext
@Order(1)
void dirtyContext() {
assertContextCacheStatistics("dirtyContext()", 1, 0, 1);
assertContextCacheStatistics("dirtyContext()", 1, 1, 0, 1);
assertThat(this.applicationContext).as("The application context should have been autowired.").isNotNull();
SpringExtensionContextCacheTests.dirtiedApplicationContext = this.applicationContext;
}
@@ -81,7 +81,7 @@ class SpringExtensionContextCacheTests {
@Test
@Order(2)
void verifyContextDirty() {
assertContextCacheStatistics("verifyContextWasDirtied()", 1, 0, 2);
assertContextCacheStatistics("verifyContextWasDirtied()", 1, 1, 0, 2);
assertThat(this.applicationContext).as("The application context should have been autowired.").isNotNull();
assertThat(this.applicationContext).as("The application context should have been 'dirtied'.").isNotSameAs(SpringExtensionContextCacheTests.dirtiedApplicationContext);
SpringExtensionContextCacheTests.dirtiedApplicationContext = this.applicationContext;
@@ -90,7 +90,7 @@ class SpringExtensionContextCacheTests {
@Test
@Order(3)
void verifyContextNotDirty() {
assertContextCacheStatistics("verifyContextWasNotDirtied()", 1, 1, 2);
assertContextCacheStatistics("verifyContextWasNotDirtied()", 1, 1, 1, 2);
assertThat(this.applicationContext).as("The application context should have been autowired.").isNotNull();
assertThat(this.applicationContext).as("The application context should NOT have been 'dirtied'.").isSameAs(SpringExtensionContextCacheTests.dirtiedApplicationContext);
}
@@ -0,0 +1,308 @@
/*
* 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 java.util.Arrays;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestClassOrder;
import org.junit.platform.engine.discovery.ClassSelector;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextCustomizerFactories;
import org.springframework.test.context.NestedTestConfiguration;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.INHERIT;
import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE;
/**
* Integration tests for pausing and restarting "unused" contexts.
*
* @author Sam Brannen
* @since 7.0
* @see TestContext#markApplicationContextUnused()
*/
class UnusedContextsIntegrationTests {
@BeforeEach
@AfterEach
void clearApplicationEvents() {
EventTracker.events.clear();
}
@Test
void topLevelTestClassesWithSharedApplicationContext() {
runTestClasses(5, TestCase1.class, TestCase2.class, TestCase3.class, TestCase4.class, TestCase5.class);
assertThat(EventTracker.events).containsExactly(
// --- TestCase1 -----------------------------------------------
// Refreshed instead of Restarted, since this is the first time
// the context is loaded.
"ContextRefreshed:TestCase1",
// No BeforeTestClass, since EventPublishingTestExecutionListener
// only publishes events for a context that has already been loaded.
"AfterTestClass:TestCase1",
"ContextStopped:TestCase1",
// --- TestCase2 -----------------------------------------------
"ContextRestarted:TestCase1",
"BeforeTestClass:TestCase2",
"AfterTestClass:TestCase2",
"ContextStopped:TestCase1",
// --- TestCase3 -----------------------------------------------
"ContextRestarted:TestCase1",
"BeforeTestClass:TestCase3",
"AfterTestClass:TestCase3",
// Closed instead of Stopped, since TestCase3 uses @DirtiesContext
"ContextClosed:TestCase1",
// --- TestCase4 -----------------------------------------------
// Refreshed instead of Restarted, since TestCase3 uses @DirtiesContext
"ContextRefreshed:TestCase4",
// No BeforeTestClass, since EventPublishingTestExecutionListener
// only publishes events for a context that has already been loaded.
"AfterTestClass:TestCase4",
"ContextStopped:TestCase4",
// --- TestCase5 -----------------------------------------------
"ContextRestarted:TestCase4",
"BeforeTestClass:TestCase5",
"AfterTestClass:TestCase5",
"ContextStopped:TestCase4"
);
}
@Test
void testClassesInNestedTestHierarchy() {
runTestClasses(5, EnclosingTestCase.class);
assertThat(EventTracker.events).containsExactly(
// --- EnclosingTestCase -------------------------------------------
"ContextRefreshed:EnclosingTestCase",
// No BeforeTestClass, since EventPublishingTestExecutionListener
// only publishes events for a context that has already been loaded.
// --- NestedTestCase ------------------------------------------
// No Refreshed or Restarted event, since NestedTestCase shares the
// active context used by EnclosingTestCase.
"BeforeTestClass:NestedTestCase",
// --- OverridingNestedTestCase1 ---------------------------
"ContextRefreshed:OverridingNestedTestCase1",
// No BeforeTestClass, since EventPublishingTestExecutionListener
// only publishes events for a context that has already been loaded.
// --- InheritingNestedTestCase ------------------------
// No Refreshed or Restarted event, since InheritingNestedTestCase
// shares the active context used by OverridingNestedTestCase1.
"BeforeTestClass:InheritingNestedTestCase",
"AfterTestClass:InheritingNestedTestCase",
// No Stopped event, since OverridingNestedTestCase1 is still
// using the context
"AfterTestClass:OverridingNestedTestCase1",
"ContextStopped:OverridingNestedTestCase1",
// --- OverridingNestedTestCase2 ---------------------------
"ContextRestarted:OverridingNestedTestCase1",
"BeforeTestClass:OverridingNestedTestCase2",
"AfterTestClass:OverridingNestedTestCase2",
"ContextStopped:OverridingNestedTestCase1",
"AfterTestClass:NestedTestCase",
// No Stopped event, since EnclosingTestCase is still using the context
"AfterTestClass:EnclosingTestCase",
"ContextStopped:EnclosingTestCase"
);
}
@Test
void testClassesWithContextHierarchies() {
runTestClasses(5,
ContextHierarchyLevel1TestCase.class,
ContextHierarchyLevel2TestCase.class,
ContextHierarchyLevel3a1TestCase.class,
ContextHierarchyLevel3a2TestCase.class,
ContextHierarchyLevel3bTestCase.class
);
assertThat(EventTracker.events).containsExactly(
// --- ContextHierarchyLevel1TestCase ------------------------------
"ContextRefreshed:ContextHierarchyLevel1TestCase",
"AfterTestClass:ContextHierarchyLevel1TestCase",
"ContextStopped:ContextHierarchyLevel1TestCase",
// --- ContextHierarchyLevel2TestCase ------------------------------
"ContextRestarted:ContextHierarchyLevel1TestCase",
"ContextRefreshed:ContextHierarchyLevel2TestCase",
"AfterTestClass:ContextHierarchyLevel2TestCase",
"ContextStopped:ContextHierarchyLevel2TestCase",
"ContextStopped:ContextHierarchyLevel1TestCase",
// --- ContextHierarchyLevel3a1TestCase -----------------------------
"ContextRestarted:ContextHierarchyLevel1TestCase",
"ContextRestarted:ContextHierarchyLevel2TestCase",
"ContextRefreshed:ContextHierarchyLevel3a1TestCase",
"AfterTestClass:ContextHierarchyLevel3a1TestCase",
"ContextStopped:ContextHierarchyLevel3a1TestCase",
"ContextStopped:ContextHierarchyLevel2TestCase",
"ContextStopped:ContextHierarchyLevel1TestCase",
// --- ContextHierarchyLevel3a2TestCase -----------------------------
"ContextRestarted:ContextHierarchyLevel1TestCase",
"ContextRestarted:ContextHierarchyLevel2TestCase",
"ContextRestarted:ContextHierarchyLevel3a1TestCase",
"BeforeTestClass:ContextHierarchyLevel3a2TestCase",
"AfterTestClass:ContextHierarchyLevel3a2TestCase",
"ContextStopped:ContextHierarchyLevel3a1TestCase",
"ContextStopped:ContextHierarchyLevel2TestCase",
"ContextStopped:ContextHierarchyLevel1TestCase",
// --- ContextHierarchyLevel3bTestCase -----------------------------
"ContextRestarted:ContextHierarchyLevel1TestCase",
"ContextRestarted:ContextHierarchyLevel2TestCase",
"ContextRefreshed:ContextHierarchyLevel3bTestCase",
"AfterTestClass:ContextHierarchyLevel3bTestCase",
"ContextStopped:ContextHierarchyLevel3bTestCase",
"ContextStopped:ContextHierarchyLevel2TestCase",
"ContextStopped:ContextHierarchyLevel1TestCase"
);
}
private static void runTestClasses(int expectedTestCount, Class<?>... classes) {
EngineTestKit.engine("junit-jupiter")
.selectors(selectClasses(classes))
.execute()
.testEvents()
.assertStatistics(stats -> stats.started(expectedTestCount).succeeded(expectedTestCount));
}
private static ClassSelector[] selectClasses(Class<?>... classes) {
return Arrays.stream(classes).map(DiscoverySelectors::selectClass).toArray(ClassSelector[]::new);
}
@SpringJUnitConfig(EventTracker.class)
@ContextCustomizerFactories(DisplayNameCustomizerFactory.class)
private abstract static class AbstractTestCase {
@Test
void test() {
// no-op
}
}
static class TestCase1 extends AbstractTestCase {
}
static class TestCase2 extends AbstractTestCase {
}
@DirtiesContext
static class TestCase3 extends AbstractTestCase {
}
static class TestCase4 extends AbstractTestCase {
}
static class TestCase5 extends AbstractTestCase {
}
@SpringJUnitConfig(EventTracker.class)
@ContextCustomizerFactories(DisplayNameCustomizerFactory.class)
@TestPropertySource(properties = "magicKey = puzzle")
static class EnclosingTestCase {
@Test
void test(@Value("${magicKey}") String magicKey) {
assertThat(magicKey).isEqualTo("puzzle");
}
@Nested
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
class NestedTestCase {
@Test
void test(@Value("${magicKey}") String magicKey) {
assertThat(magicKey).isEqualTo("puzzle");
}
/**
* Duplicates configuration of {@link OverridingNestedTestCase2}.
*/
@Nested
@Order(1)
@NestedTestConfiguration(OVERRIDE)
@SpringJUnitConfig(EventTracker.class)
@ContextCustomizerFactories(DisplayNameCustomizerFactory.class)
@TestPropertySource(properties = "magicKey = enigma")
class OverridingNestedTestCase1 {
@Test
void test(@Value("${magicKey}") String magicKey) {
assertThat(magicKey).isEqualTo("enigma");
}
@Nested
@NestedTestConfiguration(INHERIT)
class InheritingNestedTestCase {
@Test
void test(@Value("${magicKey}") String magicKey) {
assertThat(magicKey).isEqualTo("enigma");
}
}
}
/**
* Duplicates configuration of {@link OverridingNestedTestCase1}.
*/
@Nested
@Order(2)
@NestedTestConfiguration(OVERRIDE)
@SpringJUnitConfig(EventTracker.class)
@ContextCustomizerFactories(DisplayNameCustomizerFactory.class)
@TestPropertySource(properties = "magicKey = enigma")
class OverridingNestedTestCase2 {
@Test
void test(@Value("${magicKey}") String magicKey) {
assertThat(magicKey).isEqualTo("enigma");
}
}
}
}
}
@@ -55,19 +55,19 @@ class DirtiesContextInterfaceTests {
// for example, via JUnit's @Suite.
cacheHits.set(0);
cacheMisses.set(0);
assertContextCacheStatistics("BeforeClass", 0, cacheHits.get(), cacheMisses.get());
assertContextCacheStatistics("BeforeClass", 0, 0, cacheHits.get(), cacheMisses.get());
}
@AfterAll
static void verifyFinalCacheState() {
assertContextCacheStatistics("AfterClass", 0, cacheHits.get(), cacheMisses.get());
assertContextCacheStatistics("AfterClass", 0, 0, cacheHits.get(), cacheMisses.get());
}
@Test
void verifyDirtiesContextBehavior() throws Exception {
runTestClassAndAssertStats(ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase.class, 1);
assertContextCacheStatistics("after class-level @DirtiesContext with clean test method and default class mode",
0, cacheHits.get(), cacheMisses.incrementAndGet());
0, 0, cacheHits.get(), cacheMisses.incrementAndGet());
}
private void runTestClassAndAssertStats(Class<?> testClass, int expectedTestCount) throws Exception {