diff --git a/spring-test/src/main/java/org/springframework/test/context/BootstrapContext.java b/spring-test/src/main/java/org/springframework/test/context/BootstrapContext.java index fa12a797e347006cebbb2bca1b968883c9a2cf06..19a7e89acecfc25aff8f4ebb02c4831f00289dee 100644 --- a/spring-test/src/main/java/org/springframework/test/context/BootstrapContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -28,14 +28,15 @@ package org.springframework.test.context; public interface BootstrapContext { /** - * Get the {@link Class test class} for this bootstrap context. + * Get the {@linkplain Class test class} for this bootstrap context. * @return the test class (never {@code null}) */ Class getTestClass(); /** * Get the {@link CacheAwareContextLoaderDelegate} to use for transparent - * interaction with the context cache. + * interaction with the {@code ContextCache}. + * @return the context loader delegate (never {@code null}) */ CacheAwareContextLoaderDelegate getCacheAwareContextLoaderDelegate(); diff --git a/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java index d0f9c07e8991d02b33ee24b3fe7327b3f0215b83..bf84a9a80e1729e6ef5ae87c46045faa59f13ecb 100644 --- a/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -16,6 +16,8 @@ package org.springframework.test.context; +import java.lang.reflect.Constructor; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -36,6 +38,10 @@ import static org.springframework.core.annotation.AnnotationUtils.*; */ abstract class BootstrapUtils { + private static final String DEFAULT_BOOTSTRAP_CONTEXT_CLASS_NAME = "org.springframework.test.context.support.DefaultBootstrapContext"; + + private static final String DEFAULT_CACHE_AWARE_CONTEXT_LOADER_DELEGATE_CLASS_NAME = "org.springframework.test.context.support.DefaultCacheAwareContextLoaderDelegate"; + private static final String DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME = "org.springframework.test.context.support.DefaultTestContextBootstrapper"; private static final Log logger = LogFactory.getLog(BootstrapUtils.class); @@ -45,6 +51,55 @@ abstract class BootstrapUtils { /* no-op */ } + /** + * Create the {@code BootstrapContext} for the specified {@linkplain Class test class}. + * + *

Uses reflection to create a {@link org.springframework.test.context.support.DefaultBootstrapContext} + * that uses a {@link org.springframework.test.context.support.DefaultCacheAwareContextLoaderDelegate}. + * + * @param testClass the test class for which the bootstrap context should be created + * @return a new {@code BootstrapContext}; never {@code null} + */ + @SuppressWarnings("unchecked") + static BootstrapContext createBootstrapContext(Class testClass) { + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = createCacheAwareContextLoaderDelegate(); + + Class clazz = null; + try { + clazz = (Class) ClassUtils.forName(DEFAULT_BOOTSTRAP_CONTEXT_CLASS_NAME, + BootstrapUtils.class.getClassLoader()); + + Constructor constructor = clazz.getConstructor(Class.class, + CacheAwareContextLoaderDelegate.class); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Instantiating BootstrapContext using constructor [%s]", constructor)); + } + return instantiateClass(constructor, testClass, cacheAwareContextLoaderDelegate); + } + catch (Throwable t) { + throw new IllegalStateException("Could not load BootstrapContext [" + clazz + "]", t); + } + } + + @SuppressWarnings("unchecked") + private static CacheAwareContextLoaderDelegate createCacheAwareContextLoaderDelegate() { + Class clazz = null; + try { + clazz = (Class) ClassUtils.forName( + DEFAULT_CACHE_AWARE_CONTEXT_LOADER_DELEGATE_CLASS_NAME, BootstrapUtils.class.getClassLoader()); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Instantiating CacheAwareContextLoaderDelegate from class [%s]", + clazz.getName())); + } + return instantiateClass(clazz, CacheAwareContextLoaderDelegate.class); + } + catch (Throwable t) { + throw new IllegalStateException("Could not load CacheAwareContextLoaderDelegate [" + clazz + "]", t); + } + } + /** * Resolve the {@link TestContextBootstrapper} type for the test class in the * supplied {@link BootstrapContext}, instantiate it, and provide it a reference diff --git a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java index 1a59940476bf5aebc5c72389555147b5f224830a..459c5a5c9c96e9924bba60e20fd0c454dc9090b1 100644 --- a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -23,7 +23,7 @@ import org.springframework.test.annotation.DirtiesContext.HierarchyMode; /** * A {@code CacheAwareContextLoaderDelegate} is responsible for {@linkplain * #loadContext loading} and {@linkplain #closeContext closing} application - * contexts, interacting transparently with a context cache behind + * contexts, interacting transparently with a {@link ContextCache} behind * the scenes. * *

Note: {@code CacheAwareContextLoaderDelegate} does not extend the @@ -38,8 +38,10 @@ public interface CacheAwareContextLoaderDelegate { * Load the {@linkplain ApplicationContext application context} for the supplied * {@link MergedContextConfiguration} by delegating to the {@link ContextLoader} * configured in the given {@code MergedContextConfiguration}. - *

If the context is present in the context cache it will simply + *

If the context is present in the {@code ContextCache} it will simply * be returned; otherwise, it will be loaded, stored in the cache, and returned. + *

The cache statistics should be logged by invoking + * {@link ContextCache#logStatistics()}. * @param mergedContextConfiguration the merged context configuration to use * to load the application context; never {@code null} * @return the application context @@ -50,7 +52,7 @@ public interface CacheAwareContextLoaderDelegate { /** * Remove the {@linkplain ApplicationContext application context} for the - * supplied {@link MergedContextConfiguration} from the context cache + * supplied {@link MergedContextConfiguration} from the {@code ContextCache} * and {@linkplain ConfigurableApplicationContext#close() close} it if it is * an instance of {@link ConfigurableApplicationContext}. *

The semantics of the supplied {@code HierarchyMode} must be honored when diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextCache.java b/spring-test/src/main/java/org/springframework/test/context/ContextCache.java index 5f18399d80daa68f0dd7b5fac12dda16f2157540..cbf13e432a57c6d9db772c4eba356a78f1414f2c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextCache.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextCache.java @@ -16,145 +16,55 @@ package org.springframework.test.context; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; - import org.springframework.context.ApplicationContext; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.style.ToStringCreator; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; -import org.springframework.util.Assert; -import org.springframework.util.ConcurrentReferenceHashMap; /** - * Cache for Spring {@link ApplicationContext ApplicationContexts} in a test - * environment. + * {@code ContextCache} defines the public API for caching Spring + * {@link ApplicationContext ApplicationContexts} within the Spring + * TestContext Framework. * - *

Rationale

- *

Caching has significant performance benefits if initializing the context - * takes a considerable about of time. Although initializing a Spring context - * itself is very quick, some beans in a context, such as a - * {@code LocalSessionFactoryBean} for working with Hibernate, may take some - * time to initialize. Hence it often makes sense to perform that initialization - * only once per test suite. + *

A {@code ContextCache} maintains a cache of {@code ApplicationContexts} + * keyed by {@link MergedContextConfiguration} instances. * - *

Implementation Details

- *

{@code ContextCache} maintains a cache of {@code ApplicationContexts} - * keyed by {@link MergedContextConfiguration} instances. Behind the scenes, - * Spring's {@link ConcurrentReferenceHashMap} is used to store - * {@linkplain java.lang.ref.SoftReference soft references} to cached contexts - * and {@code MergedContextConfiguration} instances. + *

Rationale

+ *

Context caching can have significant performance benefits if context + * initialization is complex. So, although initializing a Spring context itself + * is typically very quick, some beans in a context — for example, an + * in-memory database or a {@code LocalSessionFactoryBean} for working with + * Hibernate — may take several seconds to initialize. Hence it often + * makes sense to perform that initialization only once per test suite. * * @author Sam Brannen * @author Juergen Hoeller - * @since 2.5 - * @see ConcurrentReferenceHashMap + * @since 4.2 */ -class ContextCache { - - /** - * Map of context keys to Spring {@code ApplicationContext} instances. - */ - private final Map contextMap = - new ConcurrentReferenceHashMap(64); - - /** - * Map of parent keys to sets of children keys, representing a top-down tree - * of context hierarchies. This information is used for determining which subtrees - * need to be recursively removed and closed when removing a context that is a parent - * of other contexts. - */ - private final Map> hierarchyMap = - new ConcurrentReferenceHashMap>(64); - - private final AtomicInteger hitCount = new AtomicInteger(); - - private final AtomicInteger missCount = new AtomicInteger(); - - /** - * Reset all state maintained by this cache. - * @see #clear() - * @see #clearStatistics() - */ - public void reset() { - synchronized (contextMap) { - clear(); - clearStatistics(); - } - } +public interface ContextCache { /** - * Clear all contexts from the cache and clear context hierarchy information as well. + * The name of the logging category used for reporting {@code ContextCache} + * statistics. */ - public void clear() { - synchronized (contextMap) { - this.contextMap.clear(); - this.hierarchyMap.clear(); - } - } + public static final String CONTEXT_CACHE_LOGGING_CATEGORY = "org.springframework.test.context.cache"; - /** - * Clear hit and miss count statistics for the cache (i.e., reset counters to zero). - */ - public void clearStatistics() { - synchronized (contextMap) { - this.hitCount.set(0); - this.missCount.set(0); - } - } /** * Determine whether there is a cached context for the given key. * @param key the context key (never {@code null}) * @return {@code true} if the cache contains a context with the given key */ - public boolean contains(MergedContextConfiguration key) { - Assert.notNull(key, "Key must not be null"); - return this.contextMap.containsKey(key); - } + boolean contains(MergedContextConfiguration key); /** * Obtain a cached {@code ApplicationContext} for the given key. - *

The {@link #getHitCount() hit} and {@link #getMissCount() miss} counts will - * be updated accordingly. + *

The {@link #getHitCount() hit} and {@link #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 */ - public ApplicationContext get(MergedContextConfiguration key) { - Assert.notNull(key, "Key must not be null"); - ApplicationContext context = this.contextMap.get(key); - if (context == null) { - this.missCount.incrementAndGet(); - } - else { - this.hitCount.incrementAndGet(); - } - return context; - } - - /** - * Get the overall hit count for this cache. - *

A hit is any access to the cache that returns a non-null - * context for the queried key. - */ - public int getHitCount() { - return this.hitCount.get(); - } - - /** - * Get the overall miss count for this cache. - *

A miss is any access to the cache that returns a {@code null} - * context for the queried key. - */ - public int getMissCount() { - return this.missCount.get(); - } + ApplicationContext get(MergedContextConfiguration key); /** * Explicitly add an {@code ApplicationContext} instance to the cache @@ -162,122 +72,79 @@ class ContextCache { * @param key the context key (never {@code null}) * @param context the {@code ApplicationContext} instance (never {@code null}) */ - public void put(MergedContextConfiguration key, ApplicationContext context) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(context, "ApplicationContext must not be null"); - - this.contextMap.put(key, context); - MergedContextConfiguration child = key; - MergedContextConfiguration parent = child.getParent(); - while (parent != null) { - Set list = this.hierarchyMap.get(parent); - if (list == null) { - list = new HashSet(); - this.hierarchyMap.put(parent, list); - } - list.add(child); - child = parent; - parent = child.getParent(); - } - } + void put(MergedContextConfiguration key, ApplicationContext context); /** * Remove the context with the given key from the cache and explicitly - * {@linkplain ConfigurableApplicationContext#close() close} it if it is an - * instance of {@link ConfigurableApplicationContext}. - *

Generally speaking, you would only call this method if you change the - * state of a singleton bean, potentially affecting future interaction with - * the context. - *

In addition, the semantics of the supplied {@code HierarchyMode} will + * {@linkplain org.springframework.context.ConfigurableApplicationContext#close() close} + * it if it is an instance of {@code ConfigurableApplicationContext}. + *

Generally speaking, this method should be called if the state of + * a singleton bean has been modified, potentially affecting future + * interaction with the context. + *

In addition, the semantics of the supplied {@code HierarchyMode} must * be honored. See the Javadoc for {@link HierarchyMode} for details. * @param key the context key; never {@code null} * @param hierarchyMode the hierarchy mode; may be {@code null} if the context * is not part of a hierarchy */ - public void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) { - Assert.notNull(key, "Key must not be null"); - - // startKey is the level at which to begin clearing the cache, depending - // on the configured hierarchy mode. - MergedContextConfiguration startKey = key; - if (hierarchyMode == HierarchyMode.EXHAUSTIVE) { - while (startKey.getParent() != null) { - startKey = startKey.getParent(); - } - } - - List removedContexts = new ArrayList(); - remove(removedContexts, startKey); + void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode); - // Remove all remaining references to any removed contexts from the - // hierarchy map. - for (MergedContextConfiguration currentKey : removedContexts) { - for (Set children : this.hierarchyMap.values()) { - children.remove(currentKey); - } - } + /** + * Determine the number of contexts currently stored in the cache. + *

If the cache contains more than {@code Integer.MAX_VALUE} elements, + * this method must return {@code Integer.MAX_VALUE}. + */ + int size(); - // Remove empty entries from the hierarchy map. - for (MergedContextConfiguration currentKey : this.hierarchyMap.keySet()) { - if (this.hierarchyMap.get(currentKey).isEmpty()) { - this.hierarchyMap.remove(currentKey); - } - } - } + /** + * Determine the number of parent contexts currently tracked within the cache. + */ + int getParentContextCount(); - private void remove(List removedContexts, MergedContextConfiguration key) { - Assert.notNull(key, "Key must not be null"); + /** + * Get the overall hit count for this cache. + *

A hit is any access to the cache that returns a non-null + * context for the queried key. + */ + int getHitCount(); - Set children = this.hierarchyMap.get(key); - if (children != null) { - for (MergedContextConfiguration child : children) { - // Recurse through lower levels - remove(removedContexts, child); - } - // Remove the set of children for the current context from the hierarchy map. - this.hierarchyMap.remove(key); - } + /** + * Get the overall miss count for this cache. + *

A miss is any access to the cache that returns a {@code null} + * context for the queried key. + */ + int getMissCount(); - // 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); - if (context instanceof ConfigurableApplicationContext) { - ((ConfigurableApplicationContext) context).close(); - } - removedContexts.add(key); - } + /** + * Reset all state maintained by this cache including statistics. + * @see #clear() + * @see #clearStatistics() + */ + void reset(); /** - * Determine the number of contexts currently stored in the cache. - *

If the cache contains more than {@code Integer.MAX_VALUE} elements, - * this method returns {@code Integer.MAX_VALUE}. + * Clear all contexts from the cache, clearing context hierarchy information as well. */ - public int size() { - return this.contextMap.size(); - } + void clear(); /** - * Determine the number of parent contexts currently tracked within the cache. + * Clear hit and miss count statistics for the cache (i.e., reset counters to zero). */ - public int getParentContextCount() { - return this.hierarchyMap.size(); - } + void clearStatistics(); /** - * Generate a text string containing the statistics for this cache. - *

Specifically, the returned string contains the {@linkplain #size}, - * {@linkplain #getHitCount() hit count}, {@linkplain #getMissCount() miss count}, - * and {@linkplain #getParentContextCount() parent context count}. - * @return the statistics for this cache + * Log the statistics for this {@code ContextCache} at {@code DEBUG} level + * using the {@value #CONTEXT_CACHE_LOGGING_CATEGORY} logging category. + *

The following information should be logged. + *

*/ - @Override - public String toString() { - return new ToStringCreator(this) - .append("size", size()) - .append("hitCount", getHitCount()) - .append("missCount", getMissCount()) - .append("parentContextCount", getParentContextCount()) - .toString(); - } + void logStatistics(); } diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java index 7252657f42b79fe7dbbfa6548d11f9a509ce4ff0..d33c3db8da4c29f27071b9c4ab5a76c6f4211853 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -19,25 +19,33 @@ package org.springframework.test.context; import java.util.List; /** - * {@code TestContextBootstrapper} defines a strategy SPI for bootstrapping the + * {@code TestContextBootstrapper} defines the SPI for bootstrapping the * Spring TestContext Framework. * - *

A custom bootstrapping strategy can be configured for a test class via - * {@link BootstrapWith @BootstrapWith}, either directly or as a meta-annotation. - * See {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration} - * for an example. - * - *

The {@link TestContextManager} uses a {@code TestContextBootstrapper} to + *

A {@code TestContextBootstrapper} is used by the {@link TestContextManager} to * {@linkplain #getTestExecutionListeners get the TestExecutionListeners} for the - * current test and to {@linkplain #buildMergedContextConfiguration build the - * merged context configuration} necessary to create the {@link TestContext} that + * current test and to {@linkplain #buildTestContext build the TestContext} that * it manages. * + *

Configuration

+ * + *

A custom bootstrapping strategy can be configured for a test class (or + * test class hierarchy) via {@link BootstrapWith @BootstrapWith}, either + * directly or as a meta-annotation. See + * {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration} + * for an example. + * + *

If a bootstrapper is not explicitly configured via {@code @BootstrapWith}, the + * {@link org.springframework.test.context.support.DefaultTestContextBootstrapper DefaultTestContextBootstrapper} + * will be used. + * + *

Implementation Notes

+ * *

Concrete implementations must provide a {@code public} no-args constructor. * - *

Note: this SPI might potentially change in the future in + *

WARNING: this SPI will likely change in the future in * order to accommodate new requirements. Implementers are therefore strongly encouraged - * not to implement this interface directly but rather to extend + * not to implement this interface directly but rather to extend * {@link org.springframework.test.context.support.AbstractTestContextBootstrapper * AbstractTestContextBootstrapper} or one of its concrete subclasses instead. * @@ -59,28 +67,13 @@ public interface TestContextBootstrapper { BootstrapContext getBootstrapContext(); /** - * Get a list of newly instantiated {@link TestExecutionListener TestExecutionListeners} - * for the test class in the {@link BootstrapContext} associated with this bootstrapper. - *

If {@link TestExecutionListeners @TestExecutionListeners} is not - * present on the test class in the {@code BootstrapContext}, - * default listeners should be returned. Furthermore, default - * listeners must be sorted using - * {@link org.springframework.core.annotation.AnnotationAwareOrderComparator - * AnnotationAwareOrderComparator}. - *

Concrete implementations are free to determine what comprises the - * set of default listeners. However, by default, the Spring TestContext - * Framework will use the - * {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader} - * mechanism to look up all {@code TestExecutionListener} class names - * configured in all {@code META-INF/spring.factories} files on the classpath. - *

The {@link TestExecutionListeners#inheritListeners() inheritListeners} - * flag of {@link TestExecutionListeners @TestExecutionListeners} must be - * taken into consideration. Specifically, if the {@code inheritListeners} - * flag is set to {@code true}, listeners declared for a given test class must - * be appended to the end of the list of listeners declared in superclasses. - * @return a list of {@code TestExecutionListener} instances + * Build the {@link TestContext} for the {@link BootstrapContext} + * associated with this bootstrapper. + * @return a new {@link TestContext}, never {@code null} + * @since 4.2 + * @see #buildMergedContextConfiguration() */ - List getTestExecutionListeners(); + TestContext buildTestContext(); /** * Build the {@linkplain MergedContextConfiguration merged context configuration} @@ -94,9 +87,12 @@ public interface TestContextBootstrapper { *

  • Active bean definition profiles declared via {@link ActiveProfiles @ActiveProfiles}
  • *
  • {@linkplain org.springframework.context.ApplicationContextInitializer * Context initializers} declared via {@link ContextConfiguration#initializers}
  • + *
  • Test property sources declared via {@link TestPropertySource @TestPropertySource}
  • * *

    Consult the Javadoc for the aforementioned annotations for details on * the required semantics. + *

    Note that the implementation of {@link #buildTestContext()} should + * typically delegate to this method when constructing the {@code TestContext}. *

    When determining which {@link ContextLoader} to use for a given test * class, the following algorithm should be used: *

      @@ -106,7 +102,32 @@ public interface TestContextBootstrapper { * {@code ContextLoader} class to use as as default. *
    * @return the merged context configuration, never {@code null} + * @see #buildTestContext() */ MergedContextConfiguration buildMergedContextConfiguration(); + /** + * Get a list of newly instantiated {@link TestExecutionListener TestExecutionListeners} + * for the test class in the {@link BootstrapContext} associated with this bootstrapper. + *

    If {@link TestExecutionListeners @TestExecutionListeners} is not + * present on the test class in the {@code BootstrapContext}, + * default listeners should be returned. Furthermore, default + * listeners must be sorted using + * {@link org.springframework.core.annotation.AnnotationAwareOrderComparator + * AnnotationAwareOrderComparator}. + *

    Concrete implementations are free to determine what comprises the + * set of default listeners. However, by default, the Spring TestContext + * Framework will use the + * {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader} + * mechanism to look up all {@code TestExecutionListener} class names + * configured in all {@code META-INF/spring.factories} files on the classpath. + *

    The {@link TestExecutionListeners#inheritListeners() inheritListeners} + * flag of {@link TestExecutionListeners @TestExecutionListeners} must be + * taken into consideration. Specifically, if the {@code inheritListeners} + * flag is set to {@code true}, listeners declared for a given test class must + * be appended to the end of the list of listeners declared in superclasses. + * @return a list of {@code TestExecutionListener} instances + */ + List getTestExecutionListeners(); + } diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java index 3551c9d03061c87fe8fb3d49b39f71df5cba6762..f217174721563a8fa7f87effc830b141cc76f864 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -28,15 +28,11 @@ import org.springframework.util.Assert; /** * {@code TestContextManager} is the main entry point into the Spring - * TestContext Framework, which provides support for loading and accessing - * {@link org.springframework.context.ApplicationContext application contexts}, - * dependency injection of test instances, - * {@link org.springframework.transaction.annotation.Transactional transactional} - * execution of test methods, etc. + * TestContext Framework. * *

    Specifically, a {@code TestContextManager} is responsible for managing a * single {@link TestContext} and signaling events to all registered - * {@link TestExecutionListener TestExecutionListeners} at well defined test + * {@link TestExecutionListener TestExecutionListeners} at the following test * execution points: * *

    * + *

    Support for loading and accessing + * {@link org.springframework.context.ApplicationContext application contexts}, + * dependency injection of test instances, + * {@link org.springframework.transaction.annotation.Transactional transactional} + * execution of test methods, etc. is provided by + * {@link SmartContextLoader ContextLoaders} and {@link TestExecutionListener + * TestExecutionListeners}, which are configured via + * {@link ContextConfiguration @ContextConfiguration} and + * {@link TestExecutionListeners @TestExecutionListeners}. + * + *

    Bootstrapping of the {@code TestContext}, the default {@code ContextLoader}, + * default {@code TestExecutionListeners}, and their collaborators is performed + * by a {@link TestContextBootstrapper}, which is configured via + * {@link BootstrapWith @BootstrapWith}. + * * @author Sam Brannen * @author Juergen Hoeller * @since 2.5 @@ -73,37 +84,42 @@ public class TestContextManager { private static final Log logger = LogFactory.getLog(TestContextManager.class); - /** - * Cache of Spring application contexts. - *

    This needs to be static, since test instances may be destroyed and - * recreated between invocations of individual test methods, as is the case - * with JUnit. - */ - static final ContextCache contextCache = new ContextCache(); - private final TestContext testContext; - private final TestContextBootstrapper testContextBootstrapper; - private final List testExecutionListeners = new ArrayList(); /** - * Construct a new {@code TestContextManager} for the specified {@linkplain Class test class} - * and automatically {@link #registerTestExecutionListeners register} the - * {@link TestExecutionListener TestExecutionListeners} configured for the test class - * via the {@link TestExecutionListeners @TestExecutionListeners} annotation. + * Construct a new {@code TestContextManager} for the specified {@linkplain Class test class}, + * automatically {@linkplain #registerTestExecutionListeners registering} the necessary + * {@link TestExecutionListener TestExecutionListeners}. + *

    Delegates to a {@link TestContextBootstrapper} for building the {@code TestContext} + * and retrieving the {@code TestExecutionListeners}. * @param testClass the test class to be managed + * @see TestContextBootstrapper#buildTestContext + * @see TestContextBootstrapper#getTestExecutionListeners * @see #registerTestExecutionListeners */ public TestContextManager(Class testClass) { - CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = new DefaultCacheAwareContextLoaderDelegate(contextCache); - BootstrapContext bootstrapContext = new DefaultBootstrapContext(testClass, cacheAwareContextLoaderDelegate); - this.testContextBootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext); - this.testContext = new DefaultTestContext(this.testContextBootstrapper); - registerTestExecutionListeners(this.testContextBootstrapper.getTestExecutionListeners()); + BootstrapContext bootstrapContext = createBootstrapContext(testClass); + TestContextBootstrapper bootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext); + this.testContext = bootstrapper.buildTestContext(); + registerTestExecutionListeners(bootstrapper.getTestExecutionListeners()); } + /** + * Create the {@code BootstrapContext} for the specified {@linkplain Class test class}. + *

    The default implementation creates a + * {@link org.springframework.test.context.support.DefaultBootstrapContext DefaultBootstrapContext} + * that uses a + * {@link org.springframework.test.context.support.DefaultCacheAwareContextLoaderDelegate DefaultCacheAwareContextLoaderDelegate}. + *

    Can be overridden by subclasses as necessary. + * @param testClass the test class for which the bootstrap context should be created + * @return a new {@code BootstrapContext}; never {@code null} + */ + protected BootstrapContext createBootstrapContext(Class testClass) { + return BootstrapUtils.createBootstrapContext(testClass); + } /** * Get the {@link TestContext} managed by this {@code TestContextManager}. @@ -201,7 +217,7 @@ public class TestContextManager { * @see #getTestExecutionListeners() */ public void prepareTestInstance(Object testInstance) throws Exception { - Assert.notNull(testInstance, "testInstance must not be null"); + Assert.notNull(testInstance, "Test instance must not be null"); if (logger.isTraceEnabled()) { logger.trace("prepareTestInstance(): instance [" + testInstance + "]"); } @@ -282,7 +298,7 @@ public class TestContextManager { * @see #getTestExecutionListeners() */ public void afterTestMethod(Object testInstance, Method testMethod, Throwable exception) throws Exception { - Assert.notNull(testInstance, "testInstance must not be null"); + Assert.notNull(testInstance, "Test instance must not be null"); if (logger.isTraceEnabled()) { logger.trace("afterTestMethod(): instance [" + testInstance + "], method [" + testMethod + "], exception [" + exception + "]"); diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java index 709c82e972ab52e09b695b79fba3ab2d383e5279..c6b1b0b15c53efb953b055498ebe33a5531e65ca 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java @@ -38,12 +38,14 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.test.context.BootstrapContext; import org.springframework.test.context.CacheAwareContextLoaderDelegate; +import org.springframework.test.context.ContextCache; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.SmartContextLoader; +import org.springframework.test.context.TestContext; import org.springframework.test.context.TestContextBootstrapper; import org.springframework.test.context.TestExecutionListener; import org.springframework.test.context.TestExecutionListeners; @@ -66,6 +68,9 @@ import org.springframework.util.StringUtils; *

  • {@link #processMergedContextConfiguration} * * + *

    To plug in custom {@link ContextCache} support, override + * {@link #getCacheAwareContextLoaderDelegate()}. + * * @author Sam Brannen * @author Juergen Hoeller * @since 4.1 @@ -93,6 +98,21 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot return this.bootstrapContext; } + /** + * Build a new {@link DefaultTestContext} using the {@linkplain Class test class} + * in the {@link BootstrapContext} associated with this bootstrapper and + * by delegating to {@link #buildMergedContextConfiguration()} and + * {@link #getCacheAwareContextLoaderDelegate()}. + *

    Concrete subclasses may choose to override this method to return a + * custom {@link TestContext} implementation. + * @since 4.2 + */ + @Override + public TestContext buildTestContext() { + return new DefaultTestContext(getBootstrapContext().getTestClass(), buildMergedContextConfiguration(), + getCacheAwareContextLoaderDelegate()); + } + /** * {@inheritDoc} */ @@ -266,7 +286,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot @Override public final MergedContextConfiguration buildMergedContextConfiguration() { Class testClass = getBootstrapContext().getTestClass(); - CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = getBootstrapContext().getCacheAwareContextLoaderDelegate(); + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = getCacheAwareContextLoaderDelegate(); if (MetaAnnotationUtils.findAnnotationDescriptorForTypes(testClass, ContextConfiguration.class, ContextHierarchy.class) == null) { @@ -455,6 +475,20 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot return null; } + /** + * Get the {@link CacheAwareContextLoaderDelegate} to use for transparent + * interaction with the {@code ContextCache}. + *

    The default implementation simply delegates to + * {@code getBootstrapContext().getCacheAwareContextLoaderDelegate()}. + *

    Concrete subclasses may choose to override this method to return a + * custom {@code CacheAwareContextLoaderDelegate} implementation with + * custom {@link ContextCache} support. + * @return the context loader delegate (never {@code null}) + */ + protected CacheAwareContextLoaderDelegate getCacheAwareContextLoaderDelegate() { + return getBootstrapContext().getCacheAwareContextLoaderDelegate(); + } + /** * Determine the default {@link ContextLoader} {@linkplain Class class} * to use for the supplied test class. diff --git a/spring-test/src/main/java/org/springframework/test/context/DefaultBootstrapContext.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultBootstrapContext.java similarity index 70% rename from spring-test/src/main/java/org/springframework/test/context/DefaultBootstrapContext.java rename to spring-test/src/main/java/org/springframework/test/context/support/DefaultBootstrapContext.java index e40802a7be10844e29c2f865b4a503bd1932dd76..d541c29b0d3f28478b58bcdd7ed62fbedfe0454f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/DefaultBootstrapContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultBootstrapContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -14,9 +14,11 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.context.support; import org.springframework.core.style.ToStringCreator; +import org.springframework.test.context.BootstrapContext; +import org.springframework.test.context.CacheAwareContextLoaderDelegate; import org.springframework.util.Assert; /** @@ -25,13 +27,19 @@ import org.springframework.util.Assert; * @author Sam Brannen * @since 4.1 */ -class DefaultBootstrapContext implements BootstrapContext { +public class DefaultBootstrapContext implements BootstrapContext { private final Class testClass; private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate; - DefaultBootstrapContext(Class testClass, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { + /** + * Construct a new {@code DefaultBootstrapContext} from the supplied arguments. + * @param testClass the test class for this bootstrap context; never {@code null} + * @param cacheAwareContextLoaderDelegate the context loader delegate to use for + * transparent interaction with the {@code ContextCache}; never {@code null} + */ + public DefaultBootstrapContext(Class testClass, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { Assert.notNull(testClass, "Test class must not be null"); Assert.notNull(cacheAwareContextLoaderDelegate, "CacheAwareContextLoaderDelegate must not be null"); this.testClass = testClass; diff --git a/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultCacheAwareContextLoaderDelegate.java similarity index 64% rename from spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java rename to spring-test/src/main/java/org/springframework/test/context/support/DefaultCacheAwareContextLoaderDelegate.java index a9d38dfa08db849a70b1d22f795f1688b8476748..7598fa9eb91f75728fd02901ec683b2cec60fafb 100644 --- a/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultCacheAwareContextLoaderDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -14,47 +14,78 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.context.support; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContext; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.test.context.CacheAwareContextLoaderDelegate; +import org.springframework.test.context.ContextCache; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.SmartContextLoader; import org.springframework.util.Assert; /** * Default implementation of the {@link CacheAwareContextLoaderDelegate} interface. * - *

    Although {@code DefaultCacheAwareContextLoaderDelegate} was first introduced - * in Spring Framework 4.1, the initial implementation of this class was extracted - * from the existing code base for {@code CacheAwareContextLoaderDelegate} when - * {@code CacheAwareContextLoaderDelegate} was converted into an interface. + *

    To use a static {@code DefaultContextCache}, invoke the + * {@link #DefaultCacheAwareContextLoaderDelegate()} constructor; otherwise, + * invoke the {@link #DefaultCacheAwareContextLoaderDelegate(ContextCache)} + * and provide a custom {@link ContextCache} implementation. * * @author Sam Brannen * @since 4.1 */ -class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContextLoaderDelegate { +public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContextLoaderDelegate { private static final Log logger = LogFactory.getLog(DefaultCacheAwareContextLoaderDelegate.class); - private static final Log statsLogger = LogFactory.getLog("org.springframework.test.context.cache"); + /** + * Default static cache of Spring application contexts. + */ + static final ContextCache defaultContextCache = new DefaultContextCache(); private final ContextCache contextCache; - DefaultCacheAwareContextLoaderDelegate(ContextCache contextCache) { + /** + * Construct a new {@code DefaultCacheAwareContextLoaderDelegate} using + * a static {@link DefaultContextCache}. + *

    This default cache is static so that each context can be cached + * and reused for all subsequent tests that declare the same unique + * context configuration within the same JVM process. + * @see #DefaultCacheAwareContextLoaderDelegate(ContextCache) + */ + public DefaultCacheAwareContextLoaderDelegate() { + this(defaultContextCache); + } + + /** + * Construct a new {@code DefaultCacheAwareContextLoaderDelegate} using + * the supplied {@link ContextCache}. + * @see #DefaultCacheAwareContextLoaderDelegate() + */ + public DefaultCacheAwareContextLoaderDelegate(ContextCache contextCache) { Assert.notNull(contextCache, "ContextCache must not be null"); this.contextCache = contextCache; } + /** + * Get the {@link ContextCache} used by this context loader delegate. + */ + protected ContextCache getContextCache() { + return this.contextCache; + } /** * Load the {@code ApplicationContext} for the supplied merged context configuration. *

    Supports both the {@link SmartContextLoader} and {@link ContextLoader} SPIs. * @throws Exception if an error occurs while loading the application context */ - private ApplicationContext loadContextInternal(MergedContextConfiguration mergedContextConfiguration) + protected ApplicationContext loadContextInternal(MergedContextConfiguration mergedContextConfiguration) throws Exception { ContextLoader contextLoader = mergedContextConfiguration.getContextLoader(); @@ -101,9 +132,7 @@ class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContextLoaderD } } - if (statsLogger.isDebugEnabled()) { - statsLogger.debug("Spring test ApplicationContext cache statistics: " + this.contextCache); - } + this.contextCache.logStatistics(); return context; } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DefaultContextCache.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultContextCache.java new file mode 100644 index 0000000000000000000000000000000000000000..bcb5c21df45d0bd1a1409491c311fecb896c84a1 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultContextCache.java @@ -0,0 +1,270 @@ +/* + * Copyright 2002-2015 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 + * + * http://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.support; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.style.ToStringCreator; +import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.test.context.ContextCache; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Default implementation of the {@link ContextCache} API. + * + *

    Uses Spring's {@link ConcurrentReferenceHashMap} to store + * {@linkplain java.lang.ref.SoftReference soft references} to cached + * contexts and {@code MergedContextConfiguration} instances. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 2.5 + * @see ConcurrentReferenceHashMap + */ +public class DefaultContextCache implements ContextCache { + + private static final Log statsLogger = LogFactory.getLog(CONTEXT_CACHE_LOGGING_CATEGORY); + + /** + * Map of context keys to Spring {@code ApplicationContext} instances. + */ + private final Map contextMap = + new ConcurrentReferenceHashMap(64); + + /** + * Map of parent keys to sets of children keys, representing a top-down tree + * of context hierarchies. This information is used for determining which subtrees + * need to be recursively removed and closed when removing a context that is a parent + * of other contexts. + */ + private final Map> hierarchyMap = + new ConcurrentReferenceHashMap>(64); + + private final AtomicInteger hitCount = new AtomicInteger(); + + private final AtomicInteger missCount = new AtomicInteger(); + + + /** + * {@inheritDoc} + */ + @Override + public boolean contains(MergedContextConfiguration key) { + Assert.notNull(key, "Key must not be null"); + return this.contextMap.containsKey(key); + } + + /** + * {@inheritDoc} + */ + @Override + public ApplicationContext get(MergedContextConfiguration key) { + Assert.notNull(key, "Key must not be null"); + ApplicationContext context = this.contextMap.get(key); + if (context == null) { + this.missCount.incrementAndGet(); + } + else { + this.hitCount.incrementAndGet(); + } + return context; + } + + /** + * {@inheritDoc} + */ + @Override + public void put(MergedContextConfiguration key, ApplicationContext context) { + Assert.notNull(key, "Key must not be null"); + Assert.notNull(context, "ApplicationContext must not be null"); + + this.contextMap.put(key, context); + MergedContextConfiguration child = key; + MergedContextConfiguration parent = child.getParent(); + while (parent != null) { + Set list = this.hierarchyMap.get(parent); + if (list == null) { + list = new HashSet(); + this.hierarchyMap.put(parent, list); + } + list.add(child); + child = parent; + parent = child.getParent(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) { + Assert.notNull(key, "Key must not be null"); + + // startKey is the level at which to begin clearing the cache, depending + // on the configured hierarchy mode. + MergedContextConfiguration startKey = key; + if (hierarchyMode == HierarchyMode.EXHAUSTIVE) { + while (startKey.getParent() != null) { + startKey = startKey.getParent(); + } + } + + List removedContexts = new ArrayList(); + remove(removedContexts, startKey); + + // Remove all remaining references to any removed contexts from the + // hierarchy map. + for (MergedContextConfiguration currentKey : removedContexts) { + for (Set children : this.hierarchyMap.values()) { + children.remove(currentKey); + } + } + + // Remove empty entries from the hierarchy map. + for (MergedContextConfiguration currentKey : this.hierarchyMap.keySet()) { + if (this.hierarchyMap.get(currentKey).isEmpty()) { + this.hierarchyMap.remove(currentKey); + } + } + } + + private void remove(List removedContexts, MergedContextConfiguration key) { + Assert.notNull(key, "Key must not be null"); + + Set children = this.hierarchyMap.get(key); + if (children != null) { + for (MergedContextConfiguration child : children) { + // Recurse through lower levels + remove(removedContexts, child); + } + // Remove the set of children for the current context from the hierarchy map. + this.hierarchyMap.remove(key); + } + + // 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); + if (context instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) context).close(); + } + removedContexts.add(key); + } + + /** + * {@inheritDoc} + */ + @Override + public int size() { + return this.contextMap.size(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getParentContextCount() { + return this.hierarchyMap.size(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getHitCount() { + return this.hitCount.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getMissCount() { + return this.missCount.get(); + } + + /** + * {@inheritDoc} + */ + @Override + public void reset() { + synchronized (contextMap) { + clear(); + clearStatistics(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() { + synchronized (contextMap) { + this.contextMap.clear(); + this.hierarchyMap.clear(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clearStatistics() { + synchronized (contextMap) { + this.hitCount.set(0); + this.missCount.set(0); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void logStatistics() { + if (statsLogger.isDebugEnabled()) { + statsLogger.debug("Spring test ApplicationContext cache statistics: " + this); + } + } + + /** + * Generate a text string containing the implementation type of this + * cache and its statistics. + *

    The string returned by this method contains all information + * required for compliance with the contract for {@link #logStatistics()}. + * @return a string representation of this cache, including statistics + */ + @Override + public String toString() { + return new ToStringCreator(this) + .append("size", size()) + .append("parentContextCount", getParentContextCount()) + .append("hitCount", getHitCount()) + .append("missCount", getMissCount()) + .toString(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java similarity index 55% rename from spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java rename to spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java index eac358e8e52d531e71527d2e4535d2411065176a..bd730b32546568345f5e47558b8c6f7db55a3cee 100644 --- a/spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.context.support; import java.lang.reflect.Method; @@ -22,21 +22,19 @@ import org.springframework.context.ApplicationContext; import org.springframework.core.AttributeAccessorSupport; import org.springframework.core.style.ToStringCreator; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.test.context.CacheAwareContextLoaderDelegate; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContext; import org.springframework.util.Assert; /** * Default implementation of the {@link TestContext} interface. * - *

    Although {@code DefaultTestContext} was first introduced in Spring Framework - * 4.0, the initial implementation of this class was extracted from the existing - * code base for {@code TestContext} when {@code TestContext} was converted into - * an interface. - * * @author Sam Brannen * @author Juergen Hoeller * @since 4.0 */ -class DefaultTestContext extends AttributeAccessorSupport implements TestContext { +public class DefaultTestContext extends AttributeAccessorSupport implements TestContext { private static final long serialVersionUID = -5827157174866681233L; @@ -54,24 +52,42 @@ class DefaultTestContext extends AttributeAccessorSupport implements TestContext /** - * Construct a new test context using the supplied {@link TestContextBootstrapper}. - * @param testContextBootstrapper the {@code TestContextBootstrapper} to use - * to construct the test context (must not be {@code null}) + * Construct a new {@code DefaultTestContext} from the supplied arguments. + * @param testClass the test class for this test context; never {@code null} + * @param mergedContextConfiguration the merged application context + * configuration for this test context; never {@code null} + * @param cacheAwareContextLoaderDelegate the delegate to use for loading + * and closing the application context for this test context; never {@code null} */ - DefaultTestContext(TestContextBootstrapper testContextBootstrapper) { - Assert.notNull(testContextBootstrapper, "TestContextBootstrapper must not be null"); - - BootstrapContext bootstrapContext = testContextBootstrapper.getBootstrapContext(); - this.testClass = bootstrapContext.getTestClass(); - this.cacheAwareContextLoaderDelegate = bootstrapContext.getCacheAwareContextLoaderDelegate(); - this.mergedContextConfiguration = testContextBootstrapper.buildMergedContextConfiguration(); + public DefaultTestContext(Class testClass, MergedContextConfiguration mergedContextConfiguration, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { + Assert.notNull(testClass, "testClass must not be null"); + Assert.notNull(mergedContextConfiguration, "MergedContextConfiguration must not be null"); + Assert.notNull(cacheAwareContextLoaderDelegate, "CacheAwareContextLoaderDelegate must not be null"); + this.testClass = testClass; + this.mergedContextConfiguration = mergedContextConfiguration; + this.cacheAwareContextLoaderDelegate = cacheAwareContextLoaderDelegate; } - + /** + * Get the {@linkplain ApplicationContext application context} for this + * test context. + *

    The default implementation delegates to the {@link CacheAwareContextLoaderDelegate} + * that was supplied when this {@code TestContext} was constructed. + * @see CacheAwareContextLoaderDelegate#loadContext + */ public ApplicationContext getApplicationContext() { return this.cacheAwareContextLoaderDelegate.loadContext(this.mergedContextConfiguration); } + /** + * Mark the {@linkplain ApplicationContext application context} associated + * with this test context as dirty (i.e., by removing it from the + * context cache and closing it). + *

    The default implementation delegates to the {@link CacheAwareContextLoaderDelegate} + * that was supplied when this {@code TestContext} was constructed. + * @see CacheAwareContextLoaderDelegate#closeContext + */ public void markApplicationContextDirty(HierarchyMode hierarchyMode) { this.cacheAwareContextLoaderDelegate.closeContext(this.mergedContextConfiguration, hierarchyMode); } diff --git a/spring-test/src/test/java/org/springframework/test/context/BootstrapTestUtils.java b/spring-test/src/test/java/org/springframework/test/context/BootstrapTestUtils.java index b3b4fa5fb40402992794c49722ebff5fb189146b..af6c4f7f5eaaba2de3ab149228ac2f70f50a4ccc 100644 --- a/spring-test/src/test/java/org/springframework/test/context/BootstrapTestUtils.java +++ b/spring-test/src/test/java/org/springframework/test/context/BootstrapTestUtils.java @@ -16,6 +16,8 @@ package org.springframework.test.context; +import org.springframework.test.context.support.DefaultBootstrapContext; + /** * Collection of test-related utility methods for working with {@link BootstrapContext * BootstrapContexts} and {@link TestContextBootstrapper TestContextBootstrappers}. diff --git a/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTestNGTests.java b/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTestNGTests.java index 9009e73229a5a8eb09c938a05d2787b9b7dc3dc5..3b45f158d6b8fa974e229aac9a2d3d16312feaf7 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTestNGTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTestNGTests.java @@ -37,7 +37,7 @@ import org.springframework.test.context.testng.TrackingTestNGTestListener; import org.testng.TestNG; import static org.junit.Assert.*; -import static org.springframework.test.context.ContextCacheTestUtils.*; +import static org.springframework.test.context.support.ContextCacheTestUtils.*; /** * JUnit 4 based integration test which verifies correct {@linkplain ContextCache @@ -79,8 +79,9 @@ public class ClassLevelDirtiesContextTestNGTests { @BeforeClass public static void verifyInitialCacheState() { - ContextCache contextCache = TestContextManager.contextCache; - contextCache.reset(); + resetContextCache(); + // Reset static counters in case tests are run multiple times in a test suite -- + // for example, via JUnit's @Suite. cacheHits.set(0); cacheMisses.set(0); assertContextCacheStatistics("BeforeClass", 0, cacheHits.get(), cacheMisses.get()); diff --git a/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTests.java b/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTests.java index 1c51ad8072e0486a693fcfc54bf8521ac5418f75..64dced85b8573bfc323a5dcda941af0169597070 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/ClassLevelDirtiesContextTests.java @@ -36,7 +36,7 @@ import org.springframework.test.context.support.DependencyInjectionTestExecution import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import static org.junit.Assert.*; -import static org.springframework.test.context.ContextCacheTestUtils.*; +import static org.springframework.test.context.support.ContextCacheTestUtils.*; /** * JUnit 4 based integration test which verifies correct {@linkplain ContextCache @@ -74,8 +74,9 @@ public class ClassLevelDirtiesContextTests { @BeforeClass public static void verifyInitialCacheState() { - ContextCache contextCache = TestContextManager.contextCache; - contextCache.reset(); + resetContextCache(); + // Reset static counters in case tests are run multiple times in a test suite -- + // for example, via JUnit's @Suite. cacheHits.set(0); cacheMisses.set(0); assertContextCacheStatistics("BeforeClass", 0, cacheHits.get(), cacheMisses.get()); diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java index 61dc9beaa3212eab80ea45456fd75d1b1fbe7f53..e8a4c3e48fdea54c7888dd7cdbc439c1cdcf3867 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/ContextCacheTests.java @@ -23,10 +23,11 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; import org.springframework.test.context.support.AnnotationConfigContextLoader; +import org.springframework.test.context.support.DefaultContextCache; import org.springframework.test.util.ReflectionTestUtils; import static org.junit.Assert.*; -import static org.springframework.test.context.ContextCacheTestUtils.*; +import static org.springframework.test.context.support.ContextCacheTestUtils.*; /** * Integration tests for verifying proper behavior of the {@link ContextCache} in @@ -39,7 +40,7 @@ import static org.springframework.test.context.ContextCacheTestUtils.*; */ public class ContextCacheTests { - private ContextCache contextCache = new ContextCache(); + private ContextCache contextCache = new DefaultContextCache(); @Before diff --git a/spring-test/src/test/java/org/springframework/test/context/SpringRunnerContextCacheTests.java b/spring-test/src/test/java/org/springframework/test/context/SpringRunnerContextCacheTests.java index 3e09f2731e90f7b43e1509a8c13ed1702065b899..622d25f88dfd0b8efff5ece1dbf3b96991ac0d10 100644 --- a/spring-test/src/test/java/org/springframework/test/context/SpringRunnerContextCacheTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/SpringRunnerContextCacheTests.java @@ -31,7 +31,7 @@ import org.springframework.test.context.support.DependencyInjectionTestExecution import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import static org.junit.Assert.*; -import static org.springframework.test.context.ContextCacheTestUtils.*; +import static org.springframework.test.context.support.ContextCacheTestUtils.*; /** * JUnit 4 based unit test which verifies correct {@link ContextCache @@ -58,8 +58,7 @@ public class SpringRunnerContextCacheTests { @BeforeClass public static void verifyInitialCacheState() { dirtiedApplicationContext = null; - ContextCache contextCache = TestContextManager.contextCache; - contextCache.reset(); + resetContextCache(); assertContextCacheStatistics("BeforeClass", 0, 0, 0); } diff --git a/spring-test/src/test/java/org/springframework/test/context/TestContextTestUtils.java b/spring-test/src/test/java/org/springframework/test/context/TestContextTestUtils.java index 3f1eb86edfbf2ba7099eba08f2aee0e25c0209ec..47ec34ebd589057c9e2c0dbb085b6223ffe7f5c0 100644 --- a/spring-test/src/test/java/org/springframework/test/context/TestContextTestUtils.java +++ b/spring-test/src/test/java/org/springframework/test/context/TestContextTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -16,6 +16,9 @@ package org.springframework.test.context; +import org.springframework.test.context.support.DefaultBootstrapContext; +import org.springframework.test.context.support.DefaultCacheAwareContextLoaderDelegate; + /** * Collection of test-related utility methods for working with {@link TestContext TestContexts}. * @@ -28,12 +31,12 @@ public abstract class TestContextTestUtils { return buildTestContext(testClass, new DefaultCacheAwareContextLoaderDelegate(contextCache)); } - public static TestContext buildTestContext( - Class testClass, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { + public static TestContext buildTestContext(Class testClass, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { BootstrapContext bootstrapContext = new DefaultBootstrapContext(testClass, cacheAwareContextLoaderDelegate); TestContextBootstrapper testContextBootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext); - return new DefaultTestContext(testContextBootstrapper); + return testContextBootstrapper.buildTestContext(); } } diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTestUtils.java b/spring-test/src/test/java/org/springframework/test/context/support/ContextCacheTestUtils.java similarity index 78% rename from spring-test/src/test/java/org/springframework/test/context/ContextCacheTestUtils.java rename to spring-test/src/test/java/org/springframework/test/context/support/ContextCacheTestUtils.java index dd5efcee1a43a7b6391ba0dbc6f0ccdc93a562f1..c6127b47c74b90cfd04047edc58250576043548c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextCacheTestUtils.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/ContextCacheTestUtils.java @@ -14,7 +14,9 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.context.support; + +import org.springframework.test.context.ContextCache; import static org.junit.Assert.*; @@ -28,7 +30,14 @@ import static org.junit.Assert.*; public class ContextCacheTestUtils { /** - * Assert the statistics of the context cache in {@link TestContextManager}. + * Reset the state of the static context cache in {@link DefaultCacheAwareContextLoaderDelegate}. + */ + public static final void resetContextCache() { + DefaultCacheAwareContextLoaderDelegate.defaultContextCache.reset(); + } + + /** + * Assert the statistics of the static context cache in {@link DefaultCacheAwareContextLoaderDelegate}. * * @param usageScenario the scenario in which the statistics are used * @param expectedSize the expected number of contexts in the cache @@ -37,8 +46,8 @@ public class ContextCacheTestUtils { */ public static final void assertContextCacheStatistics(String usageScenario, int expectedSize, int expectedHitCount, int expectedMissCount) { - assertContextCacheStatistics(TestContextManager.contextCache, usageScenario, expectedSize, expectedHitCount, - expectedMissCount); + assertContextCacheStatistics(DefaultCacheAwareContextLoaderDelegate.defaultContextCache, usageScenario, + expectedSize, expectedHitCount, expectedMissCount); } /**