diff --git a/framework-docs/modules/ROOT/nav.adoc b/framework-docs/modules/ROOT/nav.adoc
index 931133b2d95..11839cdfade 100644
--- a/framework-docs/modules/ROOT/nav.adoc
+++ b/framework-docs/modules/ROOT/nav.adoc
@@ -343,6 +343,7 @@
**** xref:testing/testcontext-framework/ctx-management/web.adoc[]
**** xref:testing/testcontext-framework/ctx-management/web-mocks.adoc[]
**** xref:testing/testcontext-framework/ctx-management/caching.adoc[]
+**** xref:testing/testcontext-framework/ctx-management/context-pausing.adoc[]
**** xref:testing/testcontext-framework/ctx-management/failure-threshold.adoc[]
**** xref:testing/testcontext-framework/ctx-management/hierarchies.adoc[]
*** xref:testing/testcontext-framework/fixture-di.adoc[]
diff --git a/framework-docs/modules/ROOT/pages/appendix.adoc b/framework-docs/modules/ROOT/pages/appendix.adoc
index f6101064014..3b367aef678 100644
--- a/framework-docs/modules/ROOT/pages/appendix.adoc
+++ b/framework-docs/modules/ROOT/pages/appendix.adoc
@@ -126,7 +126,7 @@ xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching].
| `spring.test.context.cache.pause`
| The pause mode for the context cache in the _Spring TestContext Framework_. See
-xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching].
+xref:testing/testcontext-framework/ctx-management/context-pausing.adoc[Context Pausing].
| `spring.test.context.failure.threshold`
| The failure threshold for errors encountered while attempting to load an `ApplicationContext`
diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc
index 19b4d66f2df..aa749f6b78d 100644
--- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc
+++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc
@@ -62,33 +62,6 @@ script by setting a JVM system property named `spring.test.context.cache.maxSize
alternative, you can set the same property via the
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
-As of Spring Framework 7.0, an application context stored in the context cache will be
-_paused_ when it is no longer actively in use and automatically _restarted_ the next time
-the context is retrieved from the cache. Specifically, the latter will restart all
-auto-startup beans in the application context, effectively restoring the lifecycle state.
-This ensures that background processes within the context are not actively running while
-the context is not used by tests. For example, JMS listener containers, scheduled tasks,
-and any other components in the context that implement `Lifecycle` or `SmartLifecycle`
-will be in a "stopped" state until the context is used again by a test. Note, however,
-that `SmartLifecycle` components can opt out of pausing by returning `false` from
-`SmartLifecycle#isPauseable()`.
-
-[TIP]
-====
-If you encounter issues with `Lifecycle` components that cannot or should not opt out of
-pausing, or if you discover that your test suite runs more slowly due to the pausing and
-restarting of application contexts, you can disable the pausing feature from the command
-line or a build script by setting a JVM system property named
-`spring.test.context.cache.pause` to `never`. For example:
-
-```shell
--Dspring.test.context.cache.pause=never
-```
-
-As an alternative, you can set the same property via the
-xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
-====
-
Since having a large number of application contexts loaded within a given test suite can
cause the suite to take an unnecessarily long time to run, it is often beneficial to
know exactly how many contexts have been loaded and cached. To view the statistics for
diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-pausing.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-pausing.adoc
new file mode 100644
index 00000000000..cd7fb8526f7
--- /dev/null
+++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-pausing.adoc
@@ -0,0 +1,46 @@
+[[testcontext-ctx-management-pausing]]
+= Context Pausing
+
+As of Spring Framework 7.0, an `ApplicationContext` stored in the context cache (see
+xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching]) may be
+_paused_ when it is no longer actively in use and automatically _restarted_ the next time
+the context is retrieved from the cache. Specifically, the latter will restart all
+auto-startup beans in the application context, effectively restoring the lifecycle state.
+This ensures that background processes within the context are not actively running while
+the context is not used by tests. For example, JMS listener containers, scheduled tasks,
+and any other components in the context that implement `Lifecycle` or `SmartLifecycle`
+will be in a "stopped" state until the context is used again by a test. Note, however,
+that `SmartLifecycle` components can opt out of pausing by returning `false` from
+`SmartLifecycle#isPauseable()`.
+
+You can control whether unused application contexts should be paused by setting the
+`PauseMode` to one of the following supported values.
+
+`ALWAYS` :: Always pause inactive application contexts.
+`ON_CONTEXT_SWITCH` :: Only pause inactive application contexts if the next context
+ retrieved from the context cache is a different context.
+`NEVER` :: Never pause inactive application contexts, effectively disabling the pausing
+ feature of the context cache.
+
+The `PauseMode` defaults to `ON_CONTEXT_SWITCH`, but it can be changed from the command
+line or a build script by setting a JVM system property named
+`spring.test.context.cache.pause` to one of the supported values (case insensitive). As
+an alternative, you can set the property via the
+xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
+
+For example, if you encounter issues with `Lifecycle` components that cannot or should
+not opt out of pausing, or if you discover that your test suite runs more slowly due to
+the pausing and restarting of application contexts, you can disable the pausing feature
+by setting the `spring.test.context.cache.pause` property to `never`.
+
+```shell
+-Dspring.test.context.cache.pause=never
+```
+
+Although `ON_CONTEXT_SWITCH` is the default pause mode, you still have the option to
+enable context pausing for all usage scenarios (including context switches) by setting
+the `spring.test.context.cache.pause` property to `always`.
+
+```shell
+-Dspring.test.context.cache.pause=always
+```
diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java b/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java
index 9669af1ac1b..cfbc6c44345 100644
--- a/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java
+++ b/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java
@@ -34,8 +34,9 @@ import org.springframework.util.Assert;
*
*
A {@code ContextCache} maintains a cache of {@code ApplicationContexts}
* keyed by {@link MergedContextConfiguration} instances, potentially configured
- * with a {@linkplain ContextCacheUtils#retrieveMaxCacheSize maximum size} and
- * a custom eviction policy.
+ * with a {@linkplain ContextCacheUtils#retrieveMaxCacheSize maximum size},
+ * {@linkplain ContextCacheUtils#retrievePauseMode() pause mode}, and custom
+ * eviction policy.
*
*
As of Spring Framework 6.1, this SPI includes optional support for
* {@linkplain #getFailureCount(MergedContextConfiguration) tracking} and
@@ -58,6 +59,7 @@ import org.springframework.util.Assert;
* @author Juergen Hoeller
* @since 4.2
* @see ContextCacheUtils#retrieveMaxCacheSize()
+ * @see ContextCacheUtils#retrievePauseMode()
*/
public interface ContextCache {
@@ -90,8 +92,9 @@ public interface ContextCache {
/**
* System property used to configure whether inactive application contexts
* stored in the {@link ContextCache} should be paused: {@value}.
- *
Defaults to {@code always}. Set this property to {@code never} to
- * disable pausing of inactive application contexts — for example:
+ *
Defaults to {@code on_context_switch}. Can be set to {@code always} or
+ * {@code never} to disable pausing of inactive application contexts —
+ * for example:
*
{@code -Dspring.test.context.cache.pause=never}
*
May alternatively be configured via the
* {@link org.springframework.core.SpringProperties} mechanism.
@@ -366,6 +369,7 @@ public interface ContextCache {
*
* @since 7.0.3
* @see #ALWAYS
+ * @see #ON_CONTEXT_SWITCH
* @see #NEVER
* @see ContextCache#CONTEXT_CACHE_PAUSE_PROPERTY_NAME
*/
@@ -376,6 +380,12 @@ public interface ContextCache {
*/
ALWAYS,
+ /**
+ * Only pause inactive application contexts if the next context
+ * retrieved from the cache is a different context.
+ */
+ ON_CONTEXT_SWITCH,
+
/**
* Never pause inactive application contexts, effectively disabling the
* pausing feature of the {@link ContextCache}.
diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/ContextCacheUtils.java b/spring-test/src/main/java/org/springframework/test/context/cache/ContextCacheUtils.java
index 66dae634ad5..f00051d5d6c 100644
--- a/spring-test/src/main/java/org/springframework/test/context/cache/ContextCacheUtils.java
+++ b/spring-test/src/main/java/org/springframework/test/context/cache/ContextCacheUtils.java
@@ -65,7 +65,8 @@ public abstract class ContextCacheUtils {
* Retrieve the {@link PauseMode} for the {@link ContextCache}.
*
Uses {@link SpringProperties} to retrieve a system property or Spring
* property named {@value ContextCache#CONTEXT_CACHE_PAUSE_PROPERTY_NAME}.
- *
Defaults to {@link PauseMode#ALWAYS} if no such property has been set.
+ *
Defaults to {@link PauseMode#ON_CONTEXT_SWITCH} if no such property has
+ * been set.
* @return the configured or default {@code PauseMode}
* @since 7.0.3
* @see ContextCache#CONTEXT_CACHE_PAUSE_PROPERTY_NAME
@@ -81,7 +82,7 @@ public abstract class ContextCacheUtils {
}
return pauseMode;
}
- return PauseMode.ALWAYS;
+ return PauseMode.ON_CONTEXT_SWITCH;
}
private static int retrieveProperty(String key, int defaultValue) {
diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java
index 529af02e8c9..0eb1518a853 100644
--- a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java
+++ b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java
@@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -86,6 +87,13 @@ public class DefaultContextCache implements ContextCache {
*/
private final Map>> contextUsageMap = new ConcurrentHashMap<>(32);
+ /**
+ * Set of keys for contexts that are currently unused and are therefore
+ * candidates for pausing on context switch.
+ * @since 7.0.3
+ */
+ private final Set unusedContexts = new LinkedHashSet<>(4);
+
/**
* Map of context keys to context load failure counts.
* @since 6.1
@@ -166,6 +174,7 @@ public class DefaultContextCache implements ContextCache {
}
else {
this.hitCount.incrementAndGet();
+ pauseOnContextSwitchIfNecessary(key);
restartContextIfNecessary(context);
}
return context;
@@ -191,6 +200,7 @@ public class DefaultContextCache implements ContextCache {
Assert.notNull(context, "ApplicationContext must not be null");
evictLruContextIfNecessary();
+ pauseOnContextSwitchIfNecessary(key);
putInternal(key, context);
}
@@ -200,6 +210,7 @@ public class DefaultContextCache implements ContextCache {
Assert.notNull(loadFunction, "LoadFunction must not be null");
evictLruContextIfNecessary();
+ pauseOnContextSwitchIfNecessary(key);
ApplicationContext context = loadFunction.loadContext(key);
Assert.state(context != null, "LoadFunction must return a non-null ApplicationContext");
putInternal(key, context);
@@ -253,9 +264,9 @@ public class DefaultContextCache implements ContextCache {
Set> activeTestClasses = getActiveTestClasses(mergedConfig);
activeTestClasses.remove(testClass);
if (activeTestClasses.isEmpty()) {
- if ((this.pauseMode == PauseMode.ALWAYS) &&
- (context instanceof ConfigurableApplicationContext cac && cac.isRunning())) {
- cac.pause();
+ switch (this.pauseMode) {
+ case ALWAYS -> pauseIfNecessary(context);
+ case ON_CONTEXT_SWITCH -> this.unusedContexts.add(mergedConfig);
}
this.contextUsageMap.remove(mergedConfig);
}
@@ -271,6 +282,38 @@ public class DefaultContextCache implements ContextCache {
return this.contextUsageMap.computeIfAbsent(mergedConfig, key -> new HashSet<>());
}
+ private boolean pauseOnContextSwitch() {
+ return (this.pauseMode == PauseMode.ON_CONTEXT_SWITCH);
+ }
+
+ private void pauseOnContextSwitchIfNecessary(MergedContextConfiguration activeContextKey) {
+ if (pauseOnContextSwitch()) {
+ removeFromUnusedContexts(activeContextKey);
+ for (MergedContextConfiguration unusedContextKey : this.unusedContexts) {
+ pauseIfNecessary(this.contextMap.get(unusedContextKey));
+ }
+ this.unusedContexts.clear();
+ }
+ }
+
+ /**
+ * Remove the supplied key and any keys for parent contexts from the unused
+ * contexts set. This effectively stops tracking the context (or context
+ * hierarchy) as unused.
+ */
+ private void removeFromUnusedContexts(MergedContextConfiguration key) {
+ do {
+ this.unusedContexts.remove(key);
+ key = key.getParent();
+ } while (key != null);
+ }
+
+ private static void pauseIfNecessary(@Nullable ApplicationContext context) {
+ if (context instanceof ConfigurableApplicationContext cac && cac.isRunning()) {
+ cac.pause();
+ }
+ }
+
@Override
public void remove(MergedContextConfiguration key, @Nullable HierarchyMode hierarchyMode) {
Assert.notNull(key, "Key must not be null");
@@ -322,6 +365,9 @@ public class DefaultContextCache implements ContextCache {
// stack as opposed to prior to the recursive call).
ApplicationContext context = this.contextMap.remove(key);
this.contextUsageMap.remove(key);
+ if (pauseOnContextSwitch()) {
+ this.unusedContexts.remove(key);
+ }
if (context instanceof ConfigurableApplicationContext cac) {
cac.close();
}
@@ -387,6 +433,7 @@ public class DefaultContextCache implements ContextCache {
this.contextMap.clear();
this.hierarchyMap.clear();
this.contextUsageMap.clear();
+ this.unusedContexts.clear();
}
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ContextCachePauseModeTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/ContextCachePauseModeTests.java
index 0da74caab72..377ce2d8d7b 100644
--- a/spring-test/src/test/java/org/springframework/test/context/cache/ContextCachePauseModeTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/cache/ContextCachePauseModeTests.java
@@ -103,6 +103,55 @@ class ContextCachePauseModeTests {
clearApplicationEvents();
}
+ @Test
+ void topLevelTestClassesWithPauseModeOnContextSwitch() {
+ this.contextCache = new DefaultContextCache(DEFAULT_MAX_CONTEXT_CACHE_SIZE, PauseMode.ON_CONTEXT_SWITCH);
+
+ 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("ContextPaused:TestCase1A", "ContextRefreshed:TestCase2");
+ clearApplicationEvents();
+
+ loadCtxAndAssertStats(TestCase1B.class, 2, 1, 4, 2);
+ assertThat(EventTracker.events).containsExactly("ContextPaused:TestCase2", "ContextRestarted:TestCase1A");
+ clearApplicationEvents();
+
+ loadCtxAndAssertStats(TestCase1A.class, 2, 1, 5, 2);
+ assertThat(EventTracker.events).isEmpty();
+ clearApplicationEvents();
+
+ loadCtxAndAssertStats(TestCase2.class, 2, 1, 6, 2);
+ assertThat(EventTracker.events).containsExactly("ContextPaused:TestCase1A", "ContextRestarted:TestCase2");
+ 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();
+ }
+
@Test
void topLevelTestClassesWithPauseModeNever() {
this.contextCache = new DefaultContextCache(DEFAULT_MAX_CONTEXT_CACHE_SIZE, PauseMode.NEVER);
diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheUtilsTests.java
index 2b8744ea2ca..cd9141282dd 100644
--- a/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheUtilsTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheUtilsTests.java
@@ -112,6 +112,13 @@ class ContextCacheUtilsTests {
"\talways\u000B"
};
+ static final String[] ON_CONTEXT_SWITCH_VALUES = {
+ "on_context_switch",
+ "On_Context_Switch",
+ "ON_CONTEXT_SWITCH",
+ "\ton_context_switch\u000B"
+ };
+
static final String[] NEVER_VALUES = {
"never",
"Never",
@@ -129,7 +136,7 @@ class ContextCacheUtilsTests {
@Test
void retrievePauseModeFromDefault() {
- assertThat(retrievePauseMode()).isEqualTo(PauseMode.ALWAYS);
+ assertThat(retrievePauseMode()).isEqualTo(PauseMode.ON_CONTEXT_SWITCH);
}
@Test
@@ -151,6 +158,13 @@ class ContextCacheUtilsTests {
assertThat(retrievePauseMode()).isEqualTo(PauseMode.ALWAYS);
}
+ @ParameterizedTest
+ @FieldSource("ON_CONTEXT_SWITCH_VALUES")
+ void retrievePauseModeFromSystemPropertyWithValueOnContextSwitch(String value) {
+ System.setProperty(CONTEXT_CACHE_PAUSE_PROPERTY_NAME, value);
+ assertThat(retrievePauseMode()).isEqualTo(PauseMode.ON_CONTEXT_SWITCH);
+ }
+
@ParameterizedTest
@FieldSource("NEVER_VALUES")
void retrievePauseModeFromSystemPropertyWithValueNever(String value) {
@@ -165,6 +179,13 @@ class ContextCacheUtilsTests {
assertThat(retrievePauseMode()).isEqualTo(PauseMode.ALWAYS);
}
+ @ParameterizedTest
+ @FieldSource("ON_CONTEXT_SWITCH_VALUES")
+ void retrievePauseModeFromSpringPropertyWithValueOnContextSwitch(String value) {
+ SpringProperties.setProperty(CONTEXT_CACHE_PAUSE_PROPERTY_NAME, value);
+ assertThat(retrievePauseMode()).isEqualTo(PauseMode.ON_CONTEXT_SWITCH);
+ }
+
@ParameterizedTest
@FieldSource("NEVER_VALUES")
void retrievePauseModeFromSpringPropertyWithValueNever(String value) {
diff --git a/spring-test/src/test/java/org/springframework/test/context/cache/UnusedContextsIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/cache/UnusedContextsIntegrationTests.java
index 1df14d11a62..19ad3f31c5b 100644
--- a/spring-test/src/test/java/org/springframework/test/context/cache/UnusedContextsIntegrationTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/cache/UnusedContextsIntegrationTests.java
@@ -16,6 +16,10 @@
package org.springframework.test.context.cache;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.ClassOrderer;
@@ -23,20 +27,26 @@ 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.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
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.cache.ContextCache.PauseMode;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClasses;
import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.INHERIT;
import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE;
+import static org.springframework.test.context.cache.ContextCacheTestUtils.resetContextCache;
/**
* Integration tests for pausing and restarting "unused" contexts.
@@ -49,10 +59,63 @@ class UnusedContextsIntegrationTests {
@BeforeEach
@AfterEach
- void clearApplicationEvents() {
+ void clearApplicationEventsAndResetContextCache() {
+ resetContextCache();
EventTracker.events.clear();
}
+ @Test
+ void topLevelTestClassesWithDifferentApplicationContexts() {
+ runTestClasses(6,
+ TestCaseConfig1A.class,
+ TestCaseConfig1B.class,
+ TestCaseConfig2.class,
+ TestCaseConfig3.class,
+ TestCaseConfig4.class,
+ TestCaseConfig5.class);
+
+ assertThat(EventTracker.events).containsExactly(
+
+ // --- TestCaseConfig1A --------------------------------------------
+ "ContextRefreshed:TestCaseConfig1A",
+ // No BeforeTestClass, since EventPublishingTestExecutionListener
+ // only publishes events for a context that has already been loaded.
+ "AfterTestClass:TestCaseConfig1A",
+
+ // --- TestCaseConfig1B --------------------------------------------
+ // Here we expect a BeforeTestClass event, since TestCaseConfig1B
+ // uses the same context as TestCaseConfig1A.
+ "BeforeTestClass:TestCaseConfig1B",
+ "AfterTestClass:TestCaseConfig1B",
+
+ // --- TestCaseConfig2 ---------------------------------------------
+ "ContextPaused:TestCaseConfig1A",
+ "ContextRefreshed:TestCaseConfig2",
+ "AfterTestClass:TestCaseConfig2",
+
+ // --- TestCaseConfig3 ---------------------------------------------
+ "ContextPaused:TestCaseConfig2",
+ "ContextRefreshed:TestCaseConfig3",
+ "AfterTestClass:TestCaseConfig3",
+ // Closed instead of Paused, since TestCaseConfig3 uses @DirtiesContext
+ "ContextClosed:TestCaseConfig3",
+
+ // --- TestCaseConfig4 ---------------------------------------------
+ "ContextRefreshed:TestCaseConfig4",
+ "AfterTestClass:TestCaseConfig4",
+
+ // --- TestCaseConfig5 ---------------------------------------------
+ "ContextPaused:TestCaseConfig4",
+ "ContextRefreshed:TestCaseConfig5",
+ "AfterTestClass:TestCaseConfig5"
+ );
+ }
+
+ /**
+ * Since {@link PauseMode#ON_CONTEXT_SWITCH} is now the default, there are
+ * no {@code ContextPausedEvent} or {@code ContextRestartedEvent} events
+ * when all test classes share the same context.
+ */
@Test
void topLevelTestClassesWithSharedApplicationContext() {
runTestClasses(5, TestCase1.class, TestCase2.class, TestCase3.class, TestCase4.class, TestCase5.class);
@@ -60,58 +123,63 @@ class UnusedContextsIntegrationTests {
assertThat(EventTracker.events).containsExactly(
// --- TestCase1 -----------------------------------------------
- // Refreshed instead of Restarted, since this is the first time
- // the context is loaded.
+ // Refreshed, 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",
- "ContextPaused:TestCase1",
// --- TestCase2 -----------------------------------------------
- "ContextRestarted:TestCase1",
"BeforeTestClass:TestCase2",
"AfterTestClass:TestCase2",
- "ContextPaused:TestCase1",
// --- TestCase3 -----------------------------------------------
- "ContextRestarted:TestCase1",
"BeforeTestClass:TestCase3",
"AfterTestClass:TestCase3",
- // Closed instead of Stopped, since TestCase3 uses @DirtiesContext
+ // Closed instead of Paused, since TestCase3 uses @DirtiesContext
"ContextClosed:TestCase1",
// --- TestCase4 -----------------------------------------------
- // Refreshed instead of Restarted, since TestCase3 uses @DirtiesContext
+ // Refreshed, since TestCase3 uses @DirtiesContext
"ContextRefreshed:TestCase4",
// No BeforeTestClass, since EventPublishingTestExecutionListener
// only publishes events for a context that has already been loaded.
"AfterTestClass:TestCase4",
- "ContextPaused:TestCase4",
// --- TestCase5 -----------------------------------------------
- "ContextRestarted:TestCase4",
"BeforeTestClass:TestCase5",
- "AfterTestClass:TestCase5",
- "ContextPaused:TestCase4"
+ "AfterTestClass:TestCase5"
);
}
@Test
void testClassesInNestedTestHierarchy() {
- runTestClasses(5, EnclosingTestCase.class);
+ testClassesInNestedTestHierarchy(EnclosingTestCase.class, false);
+ }
- assertThat(EventTracker.events).containsExactly(
+ @Test
+ void testClassesInNestedTestHierarchyWithTestInstanceLifecyclePerClass() {
+ testClassesInNestedTestHierarchy(TestInstancePerClassEnclosingTestCase.class, true);
+ }
+
+ private void testClassesInNestedTestHierarchy(Class> enclosingClass, boolean expectBeforeTestClassEvent) {
+ // We also run a stand-alone top-level test class after the nested hierarchy,
+ // in order to verify what happens for a context switch from a nested hierarchy
+ // to something else.
+ runTestClasses(7, enclosingClass, TestCase1.class);
+ String enclosingClassName = enclosingClass.getSimpleName();
+
+ String[] events = {
// --- EnclosingTestCase -------------------------------------------
- "ContextRefreshed:EnclosingTestCase",
+ "ContextRefreshed:" + enclosingClassName,
// 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
+ // --- NestedTestCase1 -----------------------------------------
+ // No Refreshed or Restarted event, since NestedTestCase1 shares the
// active context used by EnclosingTestCase.
- "BeforeTestClass:NestedTestCase",
+ "BeforeTestClass:NestedTestCase1",
// --- OverridingNestedTestCase1 ---------------------------
"ContextRefreshed:OverridingNestedTestCase1",
@@ -123,34 +191,64 @@ class UnusedContextsIntegrationTests {
// shares the active context used by OverridingNestedTestCase1.
"BeforeTestClass:InheritingNestedTestCase",
"AfterTestClass:InheritingNestedTestCase",
- // No Stopped event, since OverridingNestedTestCase1 is still
+ // No Paused event, since OverridingNestedTestCase1 is still
// using the context
"AfterTestClass:OverridingNestedTestCase1",
- "ContextPaused:OverridingNestedTestCase1",
+ // No Paused event, since OverridingNestedTestCase2 will reuse
+ // the context
// --- OverridingNestedTestCase2 ---------------------------
- "ContextRestarted:OverridingNestedTestCase1",
+ // No Restarted event, since OverridingNestedTestCase2 will reuse
+ // the context
"BeforeTestClass:OverridingNestedTestCase2",
"AfterTestClass:OverridingNestedTestCase2",
"ContextPaused:OverridingNestedTestCase1",
- "AfterTestClass:NestedTestCase",
- // No Stopped event, since EnclosingTestCase is still using the context
+ "AfterTestClass:NestedTestCase1",
+ // No Paused event, since EnclosingTestCase is still using the context
- "AfterTestClass:EnclosingTestCase",
- "ContextPaused:EnclosingTestCase"
- );
+ // --- NestedTestCase2 -----------------------------------------
+ // Refreshed, since this is the first time the context is loaded.
+ "ContextRefreshed:NestedTestCase2",
+ "AfterTestClass:NestedTestCase2",
+
+ // Paused, since the context used by NestedTestCase2 is no longer used,
+ // and EventPublishingTestExecutionListener.afterTestClass() "gets" the
+ // context for the enclosing class again, which constitutes a context switch.
+ "ContextPaused:NestedTestCase2",
+ "AfterTestClass:" + enclosingClassName,
+
+ // --- TestCase1 ---------------------------------------------------
+ // Paused, since the context for the enclosing class is no longer used.
+ "ContextPaused:" + enclosingClassName,
+ // Refreshed, 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",
+ };
+
+ List eventsList = new ArrayList<>();
+ Collections.addAll(eventsList, events);
+ if (expectBeforeTestClassEvent) {
+ eventsList.add(1, "BeforeTestClass:" + enclosingClassName);
+ }
+ assertThat(EventTracker.events).containsExactlyElementsOf(eventsList);
}
@Test
void testClassesWithContextHierarchies() {
- runTestClasses(5,
+ // We also run a stand-alone top-level test class after the context hierarchy,
+ // in order to verify what happens for a context switch from a context hierarchy
+ // to something else.
+ runTestClasses(6,
ContextHierarchyLevel1TestCase.class,
ContextHierarchyLevel2TestCase.class,
ContextHierarchyLevel3a1TestCase.class,
ContextHierarchyLevel3a2TestCase.class,
- ContextHierarchyLevel3bTestCase.class
+ ContextHierarchyLevel3bTestCase.class,
+ TestCase1.class
);
assertThat(EventTracker.events).containsExactly(
@@ -158,42 +256,48 @@ class UnusedContextsIntegrationTests {
// --- ContextHierarchyLevel1TestCase ------------------------------
"ContextRefreshed:ContextHierarchyLevel1TestCase",
"AfterTestClass:ContextHierarchyLevel1TestCase",
- "ContextPaused:ContextHierarchyLevel1TestCase",
+ // No Paused event, since ContextHierarchyLevel2TestCase uses
+ // ContextHierarchyLevel1TestCase as its parent.
// --- ContextHierarchyLevel2TestCase ------------------------------
- "ContextRestarted:ContextHierarchyLevel1TestCase",
"ContextRefreshed:ContextHierarchyLevel2TestCase",
"AfterTestClass:ContextHierarchyLevel2TestCase",
- "ContextPaused:ContextHierarchyLevel2TestCase",
- "ContextPaused:ContextHierarchyLevel1TestCase",
+ // No Paused events, since ContextHierarchyLevel3a1TestCase uses
+ // ContextHierarchyLevel2TestCase and ContextHierarchyLevel1TestCase
+ // as its parents.
// --- ContextHierarchyLevel3a1TestCase -----------------------------
- "ContextRestarted:ContextHierarchyLevel1TestCase",
- "ContextRestarted:ContextHierarchyLevel2TestCase",
"ContextRefreshed:ContextHierarchyLevel3a1TestCase",
"AfterTestClass:ContextHierarchyLevel3a1TestCase",
- "ContextPaused:ContextHierarchyLevel3a1TestCase",
- "ContextPaused:ContextHierarchyLevel2TestCase",
- "ContextPaused:ContextHierarchyLevel1TestCase",
+ // No Paused events, since ContextHierarchyLevel3a2TestCase also uses
+ // ContextHierarchyLevel2TestCase and ContextHierarchyLevel1TestCase
+ // as its parents.
// --- ContextHierarchyLevel3a2TestCase -----------------------------
- "ContextRestarted:ContextHierarchyLevel1TestCase",
- "ContextRestarted:ContextHierarchyLevel2TestCase",
- "ContextRestarted:ContextHierarchyLevel3a1TestCase",
"BeforeTestClass:ContextHierarchyLevel3a2TestCase",
"AfterTestClass:ContextHierarchyLevel3a2TestCase",
- "ContextPaused:ContextHierarchyLevel3a1TestCase",
- "ContextPaused:ContextHierarchyLevel2TestCase",
- "ContextPaused:ContextHierarchyLevel1TestCase",
// --- ContextHierarchyLevel3bTestCase -----------------------------
- "ContextRestarted:ContextHierarchyLevel1TestCase",
- "ContextRestarted:ContextHierarchyLevel2TestCase",
+ // We see a ContextPausedEvent here, since ContextHierarchyLevel3a1TestCase
+ // and ContextHierarchyLevel3a2TestCase are no longer active and we
+ // are "switching" to ContextHierarchyLevel3bTestCase as the active context.
+ // In other words, we pause the inactive context before refreshing the
+ // new, active context.
+ "ContextPaused:ContextHierarchyLevel3a1TestCase",
"ContextRefreshed:ContextHierarchyLevel3bTestCase",
"AfterTestClass:ContextHierarchyLevel3bTestCase",
+
+ // --- TestCase1 ---------------------------------------------------
+ // Paused, since the previous context hierarchy is no longer used.
+ // Note that the pause order is bottom up.
"ContextPaused:ContextHierarchyLevel3bTestCase",
"ContextPaused:ContextHierarchyLevel2TestCase",
- "ContextPaused:ContextHierarchyLevel1TestCase"
+ "ContextPaused:ContextHierarchyLevel1TestCase",
+ // Refreshed, 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"
);
}
@@ -233,7 +337,58 @@ class UnusedContextsIntegrationTests {
static class TestCase5 extends AbstractTestCase {
}
+ @Configuration
+ @Import(EventTracker.class)
+ static class Config1 {
+ }
+
+ @Configuration
+ @Import(EventTracker.class)
+ static class Config2 {
+ }
+
+ @Configuration
+ @Import(EventTracker.class)
+ static class Config3 {
+ }
+
+ @Configuration
+ @Import(EventTracker.class)
+ static class Config4 {
+ }
+
+ @Configuration
+ @Import(EventTracker.class)
+ static class Config5 {
+ }
+
+ @SpringJUnitConfig(Config1.class)
+ static class TestCaseConfig1A extends AbstractTestCase {
+ }
+
+ @SpringJUnitConfig(Config1.class)
+ static class TestCaseConfig1B extends AbstractTestCase {
+ }
+
+ @SpringJUnitConfig(Config2.class)
+ static class TestCaseConfig2 extends AbstractTestCase {
+ }
+
+ @SpringJUnitConfig(Config3.class)
+ @DirtiesContext
+ static class TestCaseConfig3 extends AbstractTestCase {
+ }
+
+ @SpringJUnitConfig(Config4.class)
+ static class TestCaseConfig4 extends AbstractTestCase {
+ }
+
+ @SpringJUnitConfig(Config5.class)
+ static class TestCaseConfig5 extends AbstractTestCase {
+ }
+
@SpringJUnitConfig(EventTracker.class)
+ @TestClassOrder(ClassOrderer.OrderAnnotation.class)
@ContextCustomizerFactories(DisplayNameCustomizerFactory.class)
@TestPropertySource(properties = "magicKey = puzzle")
static class EnclosingTestCase {
@@ -244,8 +399,8 @@ class UnusedContextsIntegrationTests {
}
@Nested
- @TestClassOrder(ClassOrderer.OrderAnnotation.class)
- class NestedTestCase {
+ @Order(1)
+ class NestedTestCase1 {
@Test
void test(@Value("${magicKey}") String magicKey) {
@@ -296,6 +451,24 @@ class UnusedContextsIntegrationTests {
}
}
}
+
+ @Nested
+ @Order(2)
+ @NestedTestConfiguration(OVERRIDE)
+ @SpringJUnitConfig(EventTracker.class)
+ @ContextCustomizerFactories(DisplayNameCustomizerFactory.class)
+ @TestPropertySource(properties = "magicKey = enigma2")
+ class NestedTestCase2 {
+
+ @Test
+ void test(@Value("${magicKey}") String magicKey) {
+ assertThat(magicKey).isEqualTo("enigma2");
+ }
+ }
+ }
+
+ @TestInstance(Lifecycle.PER_CLASS)
+ static class TestInstancePerClassEnclosingTestCase extends EnclosingTestCase {
}
}