Browse Source

Avoid unnecessary pausing of application contexts for tests

In commit 9711db787e, we introduced support for disabling test
application context pausing via a Spring property or JVM system
property, as follows.

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

However, users may actually be interested in keeping the pausing
feature enabled if contexts are not paused unnecessarily.

To address that, this commit introduces a new
PauseMode.ON_CONTEXT_SWITCH enum constant which is now used by default
in the DefaultContextCache.

With this new pause mode, an unused application context will no longer
be paused immediately. Instead, an unused application context will be
paused lazily the first time a different context is retrieved from or
stored in the ContextCache. This effectively means that an unused
context will not be paused at all if the next test class uses the same
context.

Although ON_CONTEXT_SWITCH is the now the default pause mode, users
still have the option to enable context pausing for all usage scenarios
(not only context switches) by setting the Spring property or JVM
system property to ALWAYS (case insensitive) — for example:

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

This commit also introduces a dedicated "Context Pausing" section in
the reference manual.

See gh-36117
Closes gh-36044
pull/36169/head
Sam Brannen 3 weeks ago
parent
commit
fa404063ab
  1. 1
      framework-docs/modules/ROOT/nav.adoc
  2. 2
      framework-docs/modules/ROOT/pages/appendix.adoc
  3. 27
      framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc
  4. 46
      framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-pausing.adoc
  5. 18
      spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java
  6. 5
      spring-test/src/main/java/org/springframework/test/context/cache/ContextCacheUtils.java
  7. 53
      spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java
  8. 49
      spring-test/src/test/java/org/springframework/test/context/cache/ContextCachePauseModeTests.java
  9. 23
      spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheUtilsTests.java
  10. 271
      spring-test/src/test/java/org/springframework/test/context/cache/UnusedContextsIntegrationTests.java

1
framework-docs/modules/ROOT/nav.adoc

@ -343,6 +343,7 @@ @@ -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[]

2
framework-docs/modules/ROOT/pages/appendix.adoc

@ -126,7 +126,7 @@ xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching]. @@ -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`

27
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 @@ -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

46
framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/context-pausing.adoc

@ -0,0 +1,46 @@ @@ -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
```

18
spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java vendored

@ -34,8 +34,9 @@ import org.springframework.util.Assert; @@ -34,8 +34,9 @@ import org.springframework.util.Assert;
*
* <p>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.
*
* <p>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; @@ -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 { @@ -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}.
* <p>Defaults to {@code always}. Set this property to {@code never} to
* disable pausing of inactive application contexts &mdash; for example:
* <p>Defaults to {@code on_context_switch}. Can be set to {@code always} or
* {@code never} to disable pausing of inactive application contexts &mdash;
* for example:
* <p>{@code -Dspring.test.context.cache.pause=never}
* <p>May alternatively be configured via the
* {@link org.springframework.core.SpringProperties} mechanism.
@ -366,6 +369,7 @@ public interface ContextCache { @@ -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 { @@ -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}.

5
spring-test/src/main/java/org/springframework/test/context/cache/ContextCacheUtils.java vendored

@ -65,7 +65,8 @@ public abstract class ContextCacheUtils { @@ -65,7 +65,8 @@ public abstract class ContextCacheUtils {
* Retrieve the {@link PauseMode} for the {@link ContextCache}.
* <p>Uses {@link SpringProperties} to retrieve a system property or Spring
* property named {@value ContextCache#CONTEXT_CACHE_PAUSE_PROPERTY_NAME}.
* <p>Defaults to {@link PauseMode#ALWAYS} if no such property has been set.
* <p>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 { @@ -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) {

53
spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java vendored

@ -21,6 +21,7 @@ import java.util.Collections; @@ -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 { @@ -86,6 +87,13 @@ public class DefaultContextCache implements ContextCache {
*/
private final Map<MergedContextConfiguration, Set<Class<?>>> 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<MergedContextConfiguration> 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 { @@ -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 { @@ -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 { @@ -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 { @@ -253,9 +264,9 @@ public class DefaultContextCache implements ContextCache {
Set<Class<?>> 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 { @@ -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 { @@ -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 { @@ -387,6 +433,7 @@ public class DefaultContextCache implements ContextCache {
this.contextMap.clear();
this.hierarchyMap.clear();
this.contextUsageMap.clear();
this.unusedContexts.clear();
}
}

49
spring-test/src/test/java/org/springframework/test/context/cache/ContextCachePauseModeTests.java vendored

@ -103,6 +103,55 @@ class ContextCachePauseModeTests { @@ -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);

23
spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheUtilsTests.java vendored

@ -112,6 +112,13 @@ class ContextCacheUtilsTests { @@ -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 { @@ -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 { @@ -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 { @@ -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) {

271
spring-test/src/test/java/org/springframework/test/context/cache/UnusedContextsIntegrationTests.java vendored

@ -16,6 +16,10 @@ @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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<String> 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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 {
}
}

Loading…
Cancel
Save