Browse Source

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
pull/35212/head
Sam Brannen 7 months ago
parent
commit
dbe89abd7b
  1. 9
      framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc
  2. 39
      spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java
  3. 23
      spring-test/src/main/java/org/springframework/test/context/TestContext.java
  4. 18
      spring-test/src/main/java/org/springframework/test/context/TestContextManager.java
  5. 78
      spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java
  6. 21
      spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java
  7. 62
      spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java
  8. 16
      spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java
  9. 32
      spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTestNGTests.java
  10. 38
      spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTests.java
  11. 36
      spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTestUtils.java
  12. 190
      spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTests.java
  13. 44
      spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel1TestCase.java
  14. 34
      spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel2TestCase.java
  15. 34
      spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel3a1TestCase.java
  16. 28
      spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel3a2TestCase.java
  17. 34
      spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel3bTestCase.java
  18. 42
      spring-test/src/test/java/org/springframework/test/context/cache/DisplayNameCustomizerFactory.java
  19. 88
      spring-test/src/test/java/org/springframework/test/context/cache/EventTracker.java
  20. 10
      spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionContextCacheTests.java
  21. 308
      spring-test/src/test/java/org/springframework/test/context/cache/UnusedContextsIntegrationTests.java
  22. 6
      spring-test/src/test/java/org/springframework/test/context/configuration/interfaces/DirtiesContextInterfaceTests.java
  23. 1
      src/checkstyle/checkstyle-suppressions.xml

9
framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc

@ -62,6 +62,15 @@ script by setting a JVM system property named `spring.test.context.cache.maxSize @@ -62,6 +62,15 @@ 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
stopped 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.
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

39
spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java

@ -29,6 +29,11 @@ import org.springframework.test.annotation.DirtiesContext.HierarchyMode; @@ -29,6 +29,11 @@ import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
* {@link org.springframework.test.context.cache.ContextCache ContextCache}
* behind the scenes.
*
* <p>As of Spring Framework 7.0, this SPI includes optional support for
* {@linkplain #registerContextUsage(MergedContextConfiguration, Class) registering} and
* {@linkplain #unregisterContextUsage(MergedContextConfiguration, Class) unregistering}
* context usage.
*
* <p>Note: {@code CacheAwareContextLoaderDelegate} does not extend the
* {@link ContextLoader} or {@link SmartContextLoader} interface.
*
@ -142,4 +147,38 @@ public interface CacheAwareContextLoaderDelegate { @@ -142,4 +147,38 @@ public interface CacheAwareContextLoaderDelegate {
*/
void closeContext(MergedContextConfiguration mergedConfig, @Nullable HierarchyMode hierarchyMode);
/**
* Register usage of the {@linkplain ApplicationContext application context}
* for the supplied {@link MergedContextConfiguration} as well as usage of the
* application context for its {@linkplain MergedContextConfiguration#getParent()
* parent}, recursively.
* <p>This is intended to be invoked whenever a
* {@link org.springframework.test.context.TestExecutionListener TestExecutionListener}
* interacts with the application context(s) on behalf of the supplied test class.
* @param key the context key; never {@code null}
* @param testClass the test class that is using the application context(s)
* @since 7.0
* @see #unregisterContextUsage(MergedContextConfiguration, Class)
*/
default void registerContextUsage(MergedContextConfiguration key, Class<?> testClass) {
/* no-op */
}
/**
* Unregister usage of the {@linkplain ApplicationContext application context}
* for the supplied {@link MergedContextConfiguration} as well as usage of the
* application context for its {@linkplain MergedContextConfiguration#getParent()
* parent}, recursively.
* <p>This informs the {@code ContextCache} that the application context(s) can
* be safely {@linkplain org.springframework.context.Lifecycle#stop() stopped}
* if no other test classes are actively using the same application context(s).
* @param key the context key; never {@code null}
* @param testClass the test class that is no longer using the application context(s)
* @since 7.0
* @see #registerContextUsage(MergedContextConfiguration, Class)
*/
default void unregisterContextUsage(MergedContextConfiguration key, Class<?> testClass) {
/* no-op */
}
}

23
spring-test/src/main/java/org/springframework/test/context/TestContext.java

@ -53,7 +53,8 @@ public interface TestContext extends AttributeAccessor, Serializable { @@ -53,7 +53,8 @@ public interface TestContext extends AttributeAccessor, Serializable {
* Determine if the {@linkplain ApplicationContext application context} for
* this test context is known to be available.
* <p>If this method returns {@code true}, a subsequent invocation of
* {@link #getApplicationContext()} should succeed.
* {@link #getApplicationContext()} or {@link #markApplicationContextUnused()}
* should succeed.
* <p>The default implementation of this method always returns {@code false}.
* Custom {@code TestContext} implementations are therefore highly encouraged
* to override this method with a more meaningful implementation. Note that
@ -62,6 +63,7 @@ public interface TestContext extends AttributeAccessor, Serializable { @@ -62,6 +63,7 @@ public interface TestContext extends AttributeAccessor, Serializable {
* @return {@code true} if the application context has already been loaded
* @since 5.2
* @see #getApplicationContext()
* @see #markApplicationContextUnused()
*/
default boolean hasApplicationContext() {
return false;
@ -77,6 +79,7 @@ public interface TestContext extends AttributeAccessor, Serializable { @@ -77,6 +79,7 @@ public interface TestContext extends AttributeAccessor, Serializable {
* @throws IllegalStateException if an error occurs while retrieving the
* application context
* @see #hasApplicationContext()
* @see #markApplicationContextUnused()
*/
ApplicationContext getApplicationContext();
@ -128,6 +131,24 @@ public interface TestContext extends AttributeAccessor, Serializable { @@ -128,6 +131,24 @@ public interface TestContext extends AttributeAccessor, Serializable {
*/
@Nullable Throwable getTestException();
/**
* Call this method to signal that the {@linkplain #getTestClass() test class}
* is no longer using the {@linkplain ApplicationContext application context}
* associated with this test context.
* <p>This informs the context cache that the application context can be
* safely {@linkplain org.springframework.context.Lifecycle#stop() stopped}
* if no other test classes are actively using the same application context.
* <p>This method is intended to be invoked after execution of the test class
* has ended and should not be invoked unless the application context for this
* test context is known to be {@linkplain #hasApplicationContext() available}.
* <p>This feature is primarily intended for use within the framework.
* @since 7.0
* @see TestContextManager#afterTestClass()
*/
default void markApplicationContextUnused() {
/* no-op */
}
/**
* Call this method to signal that the {@linkplain ApplicationContext application
* context} associated with this test context is <em>dirty</em> and should be

18
spring-test/src/main/java/org/springframework/test/context/TestContextManager.java

@ -520,10 +520,14 @@ public class TestContextManager { @@ -520,10 +520,14 @@ public class TestContextManager {
* the first exception.
* <p>Note that listeners will be executed in the opposite order in which they
* were registered.
* <p>As of Spring Framework 7.0, this method also ensures that the application
* context for the current {@link #getTestContext() TestContext} is marked as
* {@linkplain TestContext#markApplicationContextUnused() unused}.
* @throws Exception if a registered TestExecutionListener throws an exception
* @since 3.0
* @see #getTestExecutionListeners()
* @see Throwable#addSuppressed(Throwable)
* @see TestContext#markApplicationContextUnused()
*/
public void afterTestClass() throws Exception {
Class<?> testClass = getTestContext().getTestClass();
@ -550,6 +554,20 @@ public class TestContextManager { @@ -550,6 +554,20 @@ public class TestContextManager {
}
}
try {
if (getTestContext().hasApplicationContext()) {
getTestContext().markApplicationContextUnused();
}
}
catch (Throwable ex) {
if (afterTestClassException == null) {
afterTestClassException = ex;
}
else {
afterTestClassException.addSuppressed(ex);
}
}
this.testContextHolder.remove();
if (afterTestClassException != null) {

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

@ -35,7 +35,10 @@ import org.springframework.test.context.MergedContextConfiguration; @@ -35,7 +35,10 @@ import org.springframework.test.context.MergedContextConfiguration;
* <p>As of Spring Framework 6.1, this SPI includes optional support for
* {@linkplain #getFailureCount(MergedContextConfiguration) tracking} and
* {@linkplain #incrementFailureCount(MergedContextConfiguration) incrementing}
* failure counts.
* failure counts. As of Spring Framework 7.0, this SPI includes optional support for
* {@linkplain #registerContextUsage(MergedContextConfiguration, Class) registering} and
* {@linkplain #unregisterContextUsage(MergedContextConfiguration, Class) unregistering}
* context usage.
*
* <h3>Rationale</h3>
* <p>Context caching can have significant performance benefits if context
@ -88,13 +91,19 @@ public interface ContextCache { @@ -88,13 +91,19 @@ public interface ContextCache {
boolean contains(MergedContextConfiguration key);
/**
* Obtain a cached {@code ApplicationContext} for the given key.
* <p>The {@linkplain #getHitCount() hit} and {@linkplain #getMissCount() miss}
* counts must be updated accordingly.
* Obtain a cached {@link ApplicationContext} for the given key.
* <p>If the cached application context was previously
* {@linkplain org.springframework.context.Lifecycle#stop() stopped}, it
* must be
* {@linkplain org.springframework.context.support.AbstractApplicationContext#restart()
* restarted}. This applies to parent contexts as well.
* <p>In addition, the {@linkplain #getHitCount() hit} and
* {@linkplain #getMissCount() miss} counts must be updated accordingly.
* @param key the context key (never {@code null})
* @return the corresponding {@code ApplicationContext} instance, or {@code null}
* if not found in the cache
* @see #remove
* @see #unregisterContextUsage(MergedContextConfiguration, Class)
* @see #remove(MergedContextConfiguration, HierarchyMode)
*/
@Nullable ApplicationContext get(MergedContextConfiguration key);
@ -151,6 +160,64 @@ public interface ContextCache { @@ -151,6 +160,64 @@ public interface ContextCache {
* @see #getFailureCount(MergedContextConfiguration)
*/
default void incrementFailureCount(MergedContextConfiguration key) {
/* no-op */
}
/**
* Register usage of the {@link ApplicationContext} for the supplied
* {@link MergedContextConfiguration} and any of its parents.
* <p>The default implementation of this method does nothing. Concrete
* implementations are therefore highly encouraged to override this
* method, {@link #unregisterContextUsage(MergedContextConfiguration, Class)},
* and {@link #getContextUsageCount()} with appropriate behavior. Note that
* the standard {@code ContextContext} implementation in Spring overrides
* these methods appropriately.
* @param key the context key; never {@code null}
* @param testClass the test class that is using the application context(s)
* @since 7.0
* @see #unregisterContextUsage(MergedContextConfiguration, Class)
* @see #getContextUsageCount()
*/
default void registerContextUsage(MergedContextConfiguration key, Class<?> testClass) {
/* no-op */
}
/**
* Unregister usage of the {@link ApplicationContext} for the supplied
* {@link MergedContextConfiguration} and any of its parents.
* <p>If no other test classes are actively using the same application
* context(s), the application context(s) should be
* {@linkplain org.springframework.context.Lifecycle#stop() stopped}.
* <p>The default implementation of this method does nothing. Concrete
* implementations are therefore highly encouraged to override this
* method, {@link #registerContextUsage(MergedContextConfiguration, Class)},
* and {@link #getContextUsageCount()} with appropriate behavior. Note that
* the standard {@code ContextContext} implementation in Spring overrides
* these methods appropriately.
* @param key the context key; never {@code null}
* @param testClass the test class that is no longer using the application context(s)
* @since 7.0
* @see #registerContextUsage(MergedContextConfiguration, Class)
* @see #getContextUsageCount()
*/
default void unregisterContextUsage(MergedContextConfiguration key, Class<?> testClass) {
/* no-op */
}
/**
* Determine the number of contexts within the cache that are currently in use.
* <p>The default implementation of this method always returns {@code 0}.
* Concrete implementations are therefore highly encouraged to override this
* method, {@link #registerContextUsage(MergedContextConfiguration, Class)},
* and {@link #unregisterContextUsage(MergedContextConfiguration, Class)} with
* appropriate behavior. Note that the standard {@code ContextContext}
* implementation in Spring overrides these methods appropriately.
* @since 7.0
* @see #registerContextUsage(MergedContextConfiguration, Class)
* @see #unregisterContextUsage(MergedContextConfiguration, Class)
*/
default int getContextUsageCount() {
return 0;
}
/**
@ -204,6 +271,7 @@ public interface ContextCache { @@ -204,6 +271,7 @@ public interface ContextCache {
* <ul>
* <li>name of the concrete {@code ContextCache} implementation</li>
* <li>{@linkplain #size}</li>
* <li>{@linkplain #getContextUsageCount() context usage count}</li>
* <li>{@linkplain #getParentContextCount() parent context count}</li>
* <li>{@linkplain #getHitCount() hit count}</li>
* <li>{@linkplain #getMissCount() miss count}</li>

21
spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java vendored

@ -58,6 +58,11 @@ import org.springframework.util.Assert; @@ -58,6 +58,11 @@ import org.springframework.util.Assert;
* delegating to {@link ContextCacheUtils#retrieveContextFailureThreshold()} to
* obtain the threshold value to use.
*
* <p>As of Spring Framework 7.0, this class provides support for
* {@linkplain #registerContextUsage(MergedContextConfiguration, Class) registering} and
* {@linkplain #unregisterContextUsage(MergedContextConfiguration, Class) unregistering}
* context usage.
*
* @author Sam Brannen
* @since 4.1
*/
@ -204,6 +209,22 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext @@ -204,6 +209,22 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
}
}
@Override
public void registerContextUsage(MergedContextConfiguration mergedConfig, Class<?> testClass) {
mergedConfig = replaceIfNecessary(mergedConfig);
synchronized (this.contextCache) {
this.contextCache.registerContextUsage(mergedConfig, testClass);
}
}
@Override
public void unregisterContextUsage(MergedContextConfiguration mergedConfig, Class<?> testClass) {
mergedConfig = replaceIfNecessary(mergedConfig);
synchronized (this.contextCache) {
this.contextCache.unregisterContextUsage(mergedConfig, testClass);
}
}
/**
* Get the {@link ContextCache} used by this context loader delegate.
*/

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

@ -73,6 +73,13 @@ public class DefaultContextCache implements ContextCache { @@ -73,6 +73,13 @@ public class DefaultContextCache implements ContextCache {
private final Map<MergedContextConfiguration, Set<MergedContextConfiguration>> hierarchyMap =
new ConcurrentHashMap<>(32);
/**
* Map of context keys to active test classes (i.e., test classes that are actively
* using the corresponding {@link ApplicationContext}).
* @since 7.0
*/
private final Map<MergedContextConfiguration, Set<Class<?>>> contextUsageMap = new ConcurrentHashMap<>(32);
/**
* Map of context keys to context load failure counts.
* @since 6.1
@ -129,10 +136,22 @@ public class DefaultContextCache implements ContextCache { @@ -129,10 +136,22 @@ public class DefaultContextCache implements ContextCache {
}
else {
this.hitCount.incrementAndGet();
restartContextIfNecessary(context);
}
return context;
}
private void restartContextIfNecessary(ApplicationContext context) {
// Recurse up the context hierarchy first.
ApplicationContext parent = context.getParent();
if (parent != null) {
restartContextIfNecessary(parent);
}
if (context instanceof ConfigurableApplicationContext cac && !cac.isRunning()) {
cac.restart();
}
}
@Override
public void put(MergedContextConfiguration key, ApplicationContext context) {
Assert.notNull(key, "Key must not be null");
@ -149,6 +168,41 @@ public class DefaultContextCache implements ContextCache { @@ -149,6 +168,41 @@ public class DefaultContextCache implements ContextCache {
}
}
@Override
public void registerContextUsage(MergedContextConfiguration mergedConfig, Class<?> testClass) {
// Recurse up the context hierarchy first.
MergedContextConfiguration parent = mergedConfig.getParent();
if (parent != null) {
registerContextUsage(parent, testClass);
}
getActiveTestClasses(mergedConfig).add(testClass);
}
@Override
public void unregisterContextUsage(MergedContextConfiguration mergedConfig, Class<?> testClass) {
ApplicationContext context = this.contextMap.get(mergedConfig);
Assert.state(context != null, "ApplicationContext must not be null for: " + mergedConfig);
Set<Class<?>> activeTestClasses = getActiveTestClasses(mergedConfig);
activeTestClasses.remove(testClass);
if (activeTestClasses.isEmpty()) {
if (context instanceof ConfigurableApplicationContext cac && cac.isRunning()) {
cac.stop();
}
this.contextUsageMap.remove(mergedConfig);
}
// Recurse up the context hierarchy last.
MergedContextConfiguration parent = mergedConfig.getParent();
if (parent != null) {
unregisterContextUsage(parent, testClass);
}
}
private Set<Class<?>> getActiveTestClasses(MergedContextConfiguration mergedConfig) {
return this.contextUsageMap.computeIfAbsent(mergedConfig, mcc -> new HashSet<>());
}
@Override
public void remove(MergedContextConfiguration key, @Nullable HierarchyMode hierarchyMode) {
Assert.notNull(key, "Key must not be null");
@ -199,6 +253,7 @@ public class DefaultContextCache implements ContextCache { @@ -199,6 +253,7 @@ public class DefaultContextCache implements ContextCache {
// Physically remove and close leaf nodes first (i.e., on the way back up the
// stack as opposed to prior to the recursive call).
ApplicationContext context = this.contextMap.remove(key);
this.contextUsageMap.remove(key);
if (context instanceof ConfigurableApplicationContext cac) {
cac.close();
}
@ -228,6 +283,11 @@ public class DefaultContextCache implements ContextCache { @@ -228,6 +283,11 @@ public class DefaultContextCache implements ContextCache {
return this.maxSize;
}
@Override
public int getContextUsageCount() {
return this.contextUsageMap.size();
}
@Override
public int getParentContextCount() {
return this.hierarchyMap.size();
@ -258,6 +318,7 @@ public class DefaultContextCache implements ContextCache { @@ -258,6 +318,7 @@ public class DefaultContextCache implements ContextCache {
synchronized (this.contextMap) {
this.contextMap.clear();
this.hierarchyMap.clear();
this.contextUsageMap.clear();
}
}
@ -288,6 +349,7 @@ public class DefaultContextCache implements ContextCache { @@ -288,6 +349,7 @@ public class DefaultContextCache implements ContextCache {
return new ToStringCreator(this)
.append("size", size())
.append("maxSize", getMaxSize())
.append("contextUsageCount", getContextUsageCount())
.append("parentContextCount", getParentContextCount())
.append("hitCount", getHitCount())
.append("missCount", getMissCount())

16
spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java

@ -136,9 +136,25 @@ public class DefaultTestContext implements TestContext { @@ -136,9 +136,25 @@ public class DefaultTestContext implements TestContext {
from the ContextCache due to a maximum cache size policy."""
.formatted(this.mergedConfig));
}
this.cacheAwareContextLoaderDelegate.registerContextUsage(this.mergedConfig, this.testClass);
return context;
}
/**
* Mark the {@linkplain ApplicationContext application context} associated
* with this test context as <em>unused</em> so that it can be safely
* {@linkplain org.springframework.context.Lifecycle#stop() stopped} if no
* other test classes are actively using the same application context.
* <p>The default implementation delegates to the {@link CacheAwareContextLoaderDelegate}
* that was supplied when this {@code TestContext} was constructed.
* @since 7.0
* @see CacheAwareContextLoaderDelegate#unregisterContextUsage(MergedContextConfiguration, Class)
*/
@Override
public void markApplicationContextUnused() {
this.cacheAwareContextLoaderDelegate.unregisterContextUsage(this.mergedConfig, this.testClass);
}
/**
* Mark the {@linkplain ApplicationContext application context} associated
* with this test context as <em>dirty</em> (i.e., by removing it from the

32
spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTestNGTests.java vendored

@ -64,7 +64,7 @@ class ClassLevelDirtiesContextTestNGTests { @@ -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 { @@ -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 { @@ -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());
}

38
spring-test/src/test/java/org/springframework/test/context/cache/ClassLevelDirtiesContextTests.java vendored

@ -62,81 +62,81 @@ class ClassLevelDirtiesContextTests { @@ -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 { @@ -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());
}

36
spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTestUtils.java vendored

@ -44,7 +44,17 @@ public class ContextCacheTestUtils { @@ -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 { @@ -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 { @@ -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);
});
}

190
spring-test/src/test/java/org/springframework/test/context/cache/ContextCacheTests.java vendored

@ -19,13 +19,11 @@ package org.springframework.test.context.cache; @@ -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 { @@ -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 { @@ -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();
}
private void loadCtxAndAssertStats(Class<?> testClass, int expectedSize, int expectedHitCount, int expectedMissCount) {
assertThat(loadContext(testClass)).isNotNull();
assertContextCacheStatistics(contextCache, testClass.getName(), expectedSize, expectedHitCount,
expectedMissCount);
assertThat(testContext.getApplicationContext()).isNotNull();
assertContextCacheStatistics(contextCache, testClass.getName(), expectedSize, expectedActiveContextsCount,
expectedHitCount, expectedMissCount);
testContext.markApplicationContextUnused();
}
@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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 {
}
}
}

44
spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel1TestCase.java vendored

@ -0,0 +1,44 @@ @@ -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 {
}
}

34
spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel2TestCase.java vendored

@ -0,0 +1,34 @@ @@ -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 {
}
}

34
spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel3a1TestCase.java vendored

@ -0,0 +1,34 @@ @@ -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 {
}
}

28
spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel3a2TestCase.java vendored

@ -0,0 +1,28 @@ @@ -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 {
}

34
spring-test/src/test/java/org/springframework/test/context/cache/ContextHierarchyLevel3bTestCase.java vendored

@ -0,0 +1,34 @@ @@ -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 {
}
}

42
spring-test/src/test/java/org/springframework/test/context/cache/DisplayNameCustomizerFactory.java vendored

@ -0,0 +1,42 @@ @@ -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());
}
}

88
spring-test/src/test/java/org/springframework/test/context/cache/EventTracker.java vendored

@ -0,0 +1,88 @@ @@ -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());
}
}

10
spring-test/src/test/java/org/springframework/test/context/cache/SpringExtensionContextCacheTests.java vendored

@ -61,19 +61,19 @@ class SpringExtensionContextCacheTests { @@ -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 { @@ -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 { @@ -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);
}

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

@ -0,0 +1,308 @@ @@ -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");
}
}
}
}
}

6
spring-test/src/test/java/org/springframework/test/context/configuration/interfaces/DirtiesContextInterfaceTests.java

@ -55,19 +55,19 @@ class DirtiesContextInterfaceTests { @@ -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 {

1
src/checkstyle/checkstyle-suppressions.xml

@ -96,6 +96,7 @@ @@ -96,6 +96,7 @@
<suppress files="src[\\/]main[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]web[\\/]reactive[\\/]server[\\/].+" checks="IllegalImport" id="bannedHamcrestImports"/>
<suppress files="src[\\/]main[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]web[\\/]servlet[\\/]result[\\/].+Matchers" checks="IllegalImport" id="bannedHamcrestImports"/>
<!-- spring-test - test -->
<suppress files="ContextHierarchyLevel1TestCase" checks="IllegalImport" id="bannedJUnitJupiterImports"/>
<suppress files="src[\\/]test[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/].+TestNGTests" checks="IllegalImport" id="bannedTestNGImports"/>
<suppress files="src[\\/]test[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]context[\\/]aot[\\/]samples[\\/]web[\\/].+Tests" checks="IllegalImport" id="bannedHamcrestImports"/>
<suppress files="src[\\/]test[\\/]java[\\/]org[\\/]springframework[\\/]test[\\/]context[\\/]junit[\\/]jupiter[\\/]web[\\/].+Tests" checks="IllegalImport" id="bannedHamcrestImports"/>

Loading…
Cancel
Save