From a281bdbfc528187dcd1214be2d2a97e8482a8a23 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 19 Feb 2014 11:14:58 +0100 Subject: [PATCH] Introduce context bootstrap strategy in the TCF Work done in conjunction with SPR-5243 and SPR-4588 introduced physical package cycles in the spring-test module. The work performed in conjunction with SPR-9924 uses reflection to resolve these physical package cycles; however, prior to this commit the logical package cycles still remain. Furthermore, over time it has become apparent that the Spring TestContext Framework (TCF) could better serve application developers and especially third-party framework developers by providing a more flexible mechanism for "bootstrapping" the TCF. For example, prior to this commit, default TestExecutionListeners could only be registered by subclassing TestContextManager (and SpringJUnit4ClassRunner if using JUnit). Similarly, the default ContextLoader could only be set by subclassing SpringJUnit4ClassRunner for JUnit and by copying and modifying AbstractTestNGSpringContextTests for TestNG. This commit addresses the aforementioned issues by introducing a bootstrap strategy in the TestContext framework that is responsible for determining default TestExecutionListeners and the default ContextLoader in an extensible fashion. The new TestContextBootstrapper SPI also provides a mechanism for supporting various types of MergedContextConfiguration depending on the feature set of the context loaders supported by the strategy. The following provides an overview of the most significant changes in this commit. - Introduced TestContextBootstrapper strategy SPI, BootstrapContext, and @BootstrapWith. - Introduced AbstractTestContextBootstrapper, DefaultTestContextBootstrapper, and WebTestContextBootstrapper implementations of the TestContextBootstrapper SPI and extracted related reflection code from ContextLoaderUtils & TestContextManager. - Introduced BootstrapUtils for retrieving the TestContextBootstrapper from @BootstrapWith, falling back to a default if @BootstrapWith is not present. - @WebAppConfiguration is now annotated with @BootstrapWith(WebTestContextBootstrapper.class). - CacheAwareContextLoaderDelegate is now an interface with a new DefaultCacheAwareContextLoaderDelegate implementation class. - Introduced closeContext(MergedContextConfiguration, HierarchyMode) in CacheAwareContextLoaderDelegate. - DefaultTestContext now uses CacheAwareContextLoaderDelegate instead of interacting directly with the ContextCache. - DefaultTestContext now delegates to a TestContextBootstrapper for building the MergedContextConfiguration. - TestContextManager now delegates to TestContextBootstrapper for retrieving TestExecutionListeners. - Deleted TestContextManager(Class, String) constructor and SpringJUnit4ClassRunner.getDefaultContextLoaderClassName(Class) method since default ContextLoader support is now implemented by TestContextBootstrappers. - Extracted ActiveProfilesUtils from ContextLoaderUtils. - Extracted ApplicationContextInitializerUtils from ContextLoaderUtils. - MetaAnnotationUtils is now a public utility class in the test.util package. - Removed restriction in @ActiveProfiles that a custom resolver cannot be used with the 'value' or 'profiles' attributes. - Introduced DefaultActiveProfilesResolver. Issue: SPR-9955 --- .../test/context/ActiveProfiles.java | 16 +- .../test/context/BootstrapContext.java | 42 + .../test/context/BootstrapUtils.java | 92 ++ .../test/context/BootstrapWith.java | 50 ++ .../CacheAwareContextLoaderDelegate.java | 118 +-- .../test/context/ContextConfiguration.java | 2 + .../test/context/ContextLoaderUtils.java | 807 ------------------ .../test/context/DefaultBootstrapContext.java | 78 ++ ...efaultCacheAwareContextLoaderDelegate.java | 112 +++ .../test/context/DefaultTestContext.java | 59 +- .../test/context/TestContextBootstrapper.java | 105 +++ .../test/context/TestContextManager.java | 209 ++--- .../test/context/TestExecutionListeners.java | 18 +- .../junit4/SpringJUnit4ClassRunner.java | 53 +- .../AbstractTestContextBootstrapper.java | 435 ++++++++++ .../context/support/ActiveProfilesUtils.java | 152 ++++ .../ApplicationContextInitializerUtils.java | 94 ++ .../context/support/ContextLoaderUtils.java | 311 +++++++ .../DefaultActiveProfilesResolver.java | 122 +++ .../DefaultTestContextBootstrapper.java | 93 ++ .../test/context/web/WebAppConfiguration.java | 5 +- .../web/WebTestContextBootstrapper.java | 108 +++ .../MetaAnnotationUtils.java | 30 +- .../test/context/BootstrapTestUtils.java | 41 + .../test/context/ContextCacheTests.java | 52 +- .../test/context/TestContextTestUtils.java | 45 + .../context/TestExecutionListenersTests.java | 4 +- ...ltContextLoaderClassSpringRunnerTests.java | 36 +- ...aderUtilsTests$BareAnnotations-context.xml | 0 .../AbstractContextLoaderUtilsTests.java | 33 +- .../ActiveProfilesUtilsTests.java} | 32 +- ...aderUtilsConfigurationAttributesTests.java | 8 +- ...ntextLoaderUtilsContextHierarchyTests.java | 9 +- ...extLoaderUtilsContextInitializerTests.java | 21 +- .../ContextLoaderUtilsMergedConfigTests.java | 47 +- .../MetaAnnotationUtilsTests.java | 11 +- ...erriddenMetaAnnotationAttributesTests.java | 9 +- 37 files changed, 2194 insertions(+), 1265 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/context/BootstrapContext.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java delete mode 100644 spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/DefaultBootstrapContext.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/support/ApplicationContextInitializerUtils.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/support/DefaultActiveProfilesResolver.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContextBootstrapper.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java rename spring-test/src/main/java/org/springframework/test/{context => util}/MetaAnnotationUtils.java (92%) create mode 100644 spring-test/src/test/java/org/springframework/test/context/BootstrapTestUtils.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/TestContextTestUtils.java rename spring-test/src/test/java/org/springframework/test/context/{ => support}/AbstractContextLoaderUtilsTests$BareAnnotations-context.xml (100%) rename spring-test/src/test/java/org/springframework/test/context/{ => support}/AbstractContextLoaderUtilsTests.java (78%) rename spring-test/src/test/java/org/springframework/test/context/{ContextLoaderUtilsActiveProfilesTests.java => support/ActiveProfilesUtilsTests.java} (90%) rename spring-test/src/test/java/org/springframework/test/context/{ => support}/ContextLoaderUtilsConfigurationAttributesTests.java (94%) rename spring-test/src/test/java/org/springframework/test/context/{ => support}/ContextLoaderUtilsContextHierarchyTests.java (98%) rename spring-test/src/test/java/org/springframework/test/context/{ => support}/ContextLoaderUtilsContextInitializerTests.java (93%) rename spring-test/src/test/java/org/springframework/test/context/{ => support}/ContextLoaderUtilsMergedConfigTests.java (79%) rename spring-test/src/test/java/org/springframework/test/{context => util}/MetaAnnotationUtilsTests.java (98%) rename spring-test/src/test/java/org/springframework/test/{context => util}/OverriddenMetaAnnotationAttributesTests.java (95%) diff --git a/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java b/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java index c54e7c2d68b..c0f73c3fe5d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java +++ b/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -50,18 +50,16 @@ public @interface ActiveProfiles { /** * Alias for {@link #profiles}. * - *

This attribute may not be used in conjunction - * with {@link #profiles} or {@link #resolver}, but it may be used - * instead of them. + *

This attribute may not be used in conjunction with + * {@link #profiles}, but it may be used instead of {@link #profiles}. */ String[] value() default {}; /** * The bean definition profiles to activate. * - *

This attribute may not be used in conjunction - * with {@link #value} or {@link #resolver}, but it may be used - * instead of them. + *

This attribute may not be used in conjunction with + * {@link #value}, but it may be used instead of {@link #value}. */ String[] profiles() default {}; @@ -69,10 +67,6 @@ public @interface ActiveProfiles { * The type of {@link ActiveProfilesResolver} to use for resolving the active * bean definition profiles programmatically. * - *

This attribute may not be used in conjunction - * with {@link #profiles} or {@link #value}, but it may be used instead - * of them in order to resolve the active profiles programmatically. - * * @since 4.0 * @see ActiveProfilesResolver */ 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 new file mode 100644 index 00000000000..fa12a797e34 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapContext.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2014 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; + +/** + * {@code BootstrapContext} encapsulates the context in which the Spring + * TestContext Framework is bootstrapped. + * + * @author Sam Brannen + * @since 4.1 + * @see BootstrapWith + * @see TestContextBootstrapper + */ +public interface BootstrapContext { + + /** + * Get the {@link 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. + */ + 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 new file mode 100644 index 00000000000..6ef59cbcf44 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2014 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; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.ClassUtils; + +import static org.springframework.beans.BeanUtils.*; +import static org.springframework.core.annotation.AnnotationUtils.*; + +/** + * {@code BootstrapUtils} is a collection of utility methods to assist with + * bootstrapping the Spring TestContext Framework. + * + * @author Sam Brannen + * @since 4.1 + * @see BootstrapWith + * @see BootstrapContext + * @see TestContextBootstrapper + */ +abstract class BootstrapUtils { + + 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); + + + private BootstrapUtils() { + /* no-op */ + } + + /** + * Resolve the {@link TestContextBootstrapper} type for the test class in the + * supplied {@link BootstrapContext}, instantiate it, and provide it a reference + * to the {@link BootstrapContext}. + * + *

If the {@link BootstrapWith @BootstrapWith} annotation is present on + * the test class, either directly or as a meta-annotation, then its + * {@link BootstrapWith#value value} will be used as the bootstrapper type. + * Otherwise, the {@link org.springframework.test.context.support.DefaultTestContextBootstrapper + * DefaultTestContextBootstrapper} will be used. + * + * @param bootstrapContext the bootstrap context to use + * @return a fully configured {@code TestContextBootstrapper} + */ + @SuppressWarnings("unchecked") + static TestContextBootstrapper resolveTestContextBootstrapper(BootstrapContext bootstrapContext) { + Class testClass = bootstrapContext.getTestClass(); + + Class clazz = null; + try { + BootstrapWith bootstrapWith = findAnnotation(testClass, BootstrapWith.class); + if (bootstrapWith != null && !TestContextBootstrapper.class.equals(bootstrapWith.value())) { + clazz = bootstrapWith.value(); + } + else { + clazz = (Class) ClassUtils.forName( + DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, BootstrapUtils.class.getClassLoader()); + } + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Instantiating TestContextBootstrapper from class [%s]", clazz.getName())); + } + + TestContextBootstrapper testContextBootstrapper = instantiateClass(clazz, TestContextBootstrapper.class); + testContextBootstrapper.setBootstrapContext(bootstrapContext); + + return testContextBootstrapper; + } + catch (Throwable t) { + throw new IllegalStateException("Could not load TestContextBootstrapper [" + clazz + + "]. Specify @BootstrapWith's 'value' attribute " + + "or make the default bootstrapper class available.", t); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java b/spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java new file mode 100644 index 00000000000..bfd32c89316 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2014 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; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@code @BootstrapWith} defines class-level metadata that is used to determine + * how to bootstrap the Spring TestContext Framework. + * + *

This annotation may also be used as a meta-annotation to create + * custom composed annotations. + * + * @author Sam Brannen + * @since 4.1 + * @see BootstrapContext + * @see TestContextBootstrapper + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface BootstrapWith { + + /** + * The {@link TestContextBootstrapper} to use to bootstrap the Spring + * TestContext Framework. + */ + Class value() default TestContextBootstrapper.class; + +} 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 f7375def035..958b210820a 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-2013 the original author or authors. + * Copyright 2002-2014 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,97 +16,61 @@ package org.springframework.test.context; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContext; -import org.springframework.util.Assert; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.annotation.DirtiesContext.HierarchyMode; /** - * {@code CacheAwareContextLoaderDelegate} loads application contexts from - * {@link MergedContextConfiguration} by delegating to the - * {@link ContextLoader} configured in the {@code MergedContextConfiguration} - * and interacting transparently with the {@link ContextCache} behind the scenes. + * A {@code CacheAwareContextLoaderDelegate} is responsible for {@linkplain + * #loadContext loading} and {@linkplain #closeContext closing} application + * contexts, interacting transparently with a context cache behind + * the scenes. * - *

Note: {@code CacheAwareContextLoaderDelegate} does not implement the + *

Note: {@code CacheAwareContextLoaderDelegate} does not extend the * {@link ContextLoader} or {@link SmartContextLoader} interface. * * @author Sam Brannen * @since 3.2.2 */ -public class CacheAwareContextLoaderDelegate { - - private static final Log logger = LogFactory.getLog(CacheAwareContextLoaderDelegate.class); - - private final ContextCache contextCache; - - - CacheAwareContextLoaderDelegate(ContextCache contextCache) { - Assert.notNull(contextCache, "ContextCache must not be null"); - this.contextCache = contextCache; - } +public interface CacheAwareContextLoaderDelegate { /** - * 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) - throws Exception { - ContextLoader contextLoader = mergedContextConfiguration.getContextLoader(); - Assert.notNull(contextLoader, "Cannot load an ApplicationContext with a NULL 'contextLoader'. " - + "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy."); - - ApplicationContext applicationContext; - - if (contextLoader instanceof SmartContextLoader) { - SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader; - applicationContext = smartContextLoader.loadContext(mergedContextConfiguration); - } - else { - String[] locations = mergedContextConfiguration.getLocations(); - Assert.notNull(locations, "Cannot load an ApplicationContext with a NULL 'locations' array. " - + "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy."); - applicationContext = contextLoader.loadContext(locations); - } - - return applicationContext; - } - - /** - * Load the {@link ApplicationContext application context} for the supplied - * merged context configuration. + * 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 + * be returned; otherwise, it will be loaded, stored in the cache, and returned. * - *

If the context is present in the cache it will simply be returned; - * otherwise, it will be loaded, stored in the cache, and returned. + * @param mergedContextConfiguration the merged context configuration to use + * to load the application context; never {@code null} * @return the application context * @throws IllegalStateException if an error occurs while retrieving or * loading the application context */ - public ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration) { - synchronized (contextCache) { - ApplicationContext context = contextCache.get(mergedContextConfiguration); - if (context == null) { - try { - context = loadContextInternal(mergedContextConfiguration); - if (logger.isDebugEnabled()) { - logger.debug(String.format("Storing ApplicationContext in cache under key [%s].", - mergedContextConfiguration)); - } - contextCache.put(mergedContextConfiguration, context); - } - catch (Exception ex) { - throw new IllegalStateException("Failed to load ApplicationContext", ex); - } - } - else { - if (logger.isDebugEnabled()) { - logger.debug(String.format("Retrieved ApplicationContext from cache with key [%s].", - mergedContextConfiguration)); - } - } - return context; - } - } + ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration); + + /** + * Remove the {@linkplain ApplicationContext application context} for the + * supplied {@link MergedContextConfiguration} from the context cache + * 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 + * removing the context from the cache. See the Javadoc for {@link HierarchyMode} + * for details. + * + *

Generally speaking, this method should only be called if the state of + * a singleton bean has been changed (potentially affecting future interaction + * with the context) or if the context needs to be prematurely removed from + * the cache. + * + * @param mergedContextConfiguration the merged context configuration for the + * application context to close; never {@code null} + * @param hierarchyMode the hierarchy mode; may be {@code null} if the context + * is not part of a hierarchy + * @since 4.1 + */ + void closeContext(MergedContextConfiguration mergedContextConfiguration, HierarchyMode hierarchyMode); } diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java index 1b3ea9f2741..a716284a90a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java @@ -91,6 +91,7 @@ public @interface ContextConfiguration { * *

This attribute may not be used in conjunction with * {@link #locations}, but it may be used instead of {@link #locations}. + * * @since 3.0 * @see #inheritLocations */ @@ -120,6 +121,7 @@ public @interface ContextConfiguration { * *

This attribute may not be used in conjunction with * {@link #value}, but it may be used instead of {@link #value}. + * * @since 2.5 * @see #inheritLocations */ diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java b/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java deleted file mode 100644 index a3bd700d35d..00000000000 --- a/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java +++ /dev/null @@ -1,807 +0,0 @@ -/* - * Copyright 2002-2014 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; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Constructor; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.annotation.AnnotationAttributes; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.test.context.MetaAnnotationUtils.AnnotationDescriptor; -import org.springframework.test.context.MetaAnnotationUtils.UntypedAnnotationDescriptor; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -import static org.springframework.beans.BeanUtils.*; -import static org.springframework.core.annotation.AnnotationUtils.*; -import static org.springframework.test.context.MetaAnnotationUtils.*; - -/** - * Utility methods for working with {@link ContextLoader ContextLoaders} and - * {@link SmartContextLoader SmartContextLoaders} and resolving resource locations, - * annotated classes, active bean definition profiles, and application context - * initializers. - * - * @author Sam Brannen - * @author Michail Nikolaev - * @since 3.1 - * @see ContextLoader - * @see SmartContextLoader - * @see ContextConfiguration - * @see ContextConfigurationAttributes - * @see ActiveProfiles - * @see ActiveProfilesResolver - * @see ApplicationContextInitializer - * @see ContextHierarchy - * @see MergedContextConfiguration - */ -abstract class ContextLoaderUtils { - - static final String GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX = "ContextHierarchyLevel#"; - - private static final Log logger = LogFactory.getLog(ContextLoaderUtils.class); - - private static final String DEFAULT_CONTEXT_LOADER_CLASS_NAME = "org.springframework.test.context.support.DelegatingSmartContextLoader"; - private static final String DEFAULT_WEB_CONTEXT_LOADER_CLASS_NAME = "org.springframework.test.context.web.WebDelegatingSmartContextLoader"; - - private static final String WEB_APP_CONFIGURATION_CLASS_NAME = "org.springframework.test.context.web.WebAppConfiguration"; - private static final String WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME = "org.springframework.test.context.web.WebMergedContextConfiguration"; - - - private ContextLoaderUtils() { - /* no-op */ - } - - /** - * Resolve the {@link ContextLoader} {@linkplain Class class} to use for the supplied - * list of {@link ContextConfigurationAttributes} and then instantiate and return that - * {@code ContextLoader}. - * - *

If the supplied {@code defaultContextLoaderClassName} is {@code null} or - * empty, depending on the absence or presence of - * {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration} either - * {@code "org.springframework.test.context.support.DelegatingSmartContextLoader"} or - * {@code "org.springframework.test.context.web.WebDelegatingSmartContextLoader"} will - * be used as the default context loader class name. For details on the class - * resolution process, see {@link #resolveContextLoaderClass}. - * - * @param testClass the test class for which the {@code ContextLoader} should be - * resolved; must not be {@code null} - * @param configAttributesList the list of configuration attributes to process; must - * not be {@code null} or empty; must be ordered bottom-up - * (i.e., as if we were traversing up the class hierarchy) - * @param defaultContextLoaderClassName the name of the default {@code ContextLoader} - * class to use; may be {@code null} or empty - * @return the resolved {@code ContextLoader} for the supplied {@code testClass} - * (never {@code null}) - * @see #resolveContextLoaderClass - */ - static ContextLoader resolveContextLoader(Class testClass, - List configAttributesList, String defaultContextLoaderClassName) { - Assert.notNull(testClass, "Class must not be null"); - Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty"); - - if (!StringUtils.hasText(defaultContextLoaderClassName)) { - Class webAppConfigClass = loadWebAppConfigurationClass(); - defaultContextLoaderClassName = webAppConfigClass != null - && findAnnotation(testClass, webAppConfigClass) != null ? DEFAULT_WEB_CONTEXT_LOADER_CLASS_NAME - : DEFAULT_CONTEXT_LOADER_CLASS_NAME; - } - - Class contextLoaderClass = resolveContextLoaderClass(testClass, configAttributesList, - defaultContextLoaderClassName); - - return instantiateClass(contextLoaderClass, ContextLoader.class); - } - - /** - * Resolve the {@link ContextLoader} {@linkplain Class class} to use for the supplied - * list of {@link ContextConfigurationAttributes}. - * - *

Beginning with the first level in the context configuration attributes hierarchy: - * - *

    - *
  1. If the {@link ContextConfigurationAttributes#getContextLoaderClass() - * contextLoaderClass} property of {@link ContextConfigurationAttributes} is - * configured with an explicit class, that class will be returned.
  2. - *
  3. If an explicit {@code ContextLoader} class is not specified at the current - * level in the hierarchy, traverse to the next level in the hierarchy and return to - * step #1.
  4. - *
  5. If no explicit {@code ContextLoader} class is found after traversing the - * hierarchy, an attempt will be made to load and return the class with the supplied - * {@code defaultContextLoaderClassName}.
  6. - *
- * - * @param testClass the class for which to resolve the {@code ContextLoader} class; - * must not be {@code null}; only used for logging purposes - * @param configAttributesList the list of configuration attributes to process; must - * not be {@code null} or empty; must be ordered bottom-up - * (i.e., as if we were traversing up the class hierarchy) - * @param defaultContextLoaderClassName the name of the default {@code ContextLoader} - * class to use; must not be {@code null} or empty - * @return the {@code ContextLoader} class to use for the supplied test class - * @throws IllegalArgumentException if {@code @ContextConfiguration} is not - * present on the supplied test class - * @throws IllegalStateException if the default {@code ContextLoader} class could not - * be loaded - */ - @SuppressWarnings("unchecked") - static Class resolveContextLoaderClass(Class testClass, - List configAttributesList, String defaultContextLoaderClassName) { - Assert.notNull(testClass, "Class must not be null"); - Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty"); - Assert.hasText(defaultContextLoaderClassName, "Default ContextLoader class name must not be null or empty"); - - for (ContextConfigurationAttributes configAttributes : configAttributesList) { - if (logger.isTraceEnabled()) { - logger.trace(String.format("Processing ContextLoader for context configuration attributes %s", - configAttributes)); - } - - Class contextLoaderClass = configAttributes.getContextLoaderClass(); - if (!ContextLoader.class.equals(contextLoaderClass)) { - if (logger.isDebugEnabled()) { - logger.debug(String.format( - "Found explicit ContextLoader class [%s] for context configuration attributes %s", - contextLoaderClass.getName(), configAttributes)); - } - return contextLoaderClass; - } - } - - try { - if (logger.isTraceEnabled()) { - logger.trace(String.format("Using default ContextLoader class [%s] for test class [%s]", - defaultContextLoaderClassName, testClass.getName())); - } - return (Class) ClassUtils.forName(defaultContextLoaderClassName, - ContextLoaderUtils.class.getClassLoader()); - } - catch (Throwable t) { - throw new IllegalStateException("Could not load default ContextLoader class [" - + defaultContextLoaderClassName + "]. Specify @ContextConfiguration's 'loader' " - + "attribute or make the default loader class available.", t); - } - } - - /** - * Convenience method for creating a {@link ContextConfigurationAttributes} - * instance from the supplied {@link ContextConfiguration} annotation and - * declaring class and then adding the attributes to the supplied list. - */ - private static void convertContextConfigToConfigAttributesAndAddToList(ContextConfiguration contextConfiguration, - Class declaringClass, final List attributesList) { - if (logger.isTraceEnabled()) { - logger.trace(String.format("Retrieved @ContextConfiguration [%s] for declaring class [%s].", - contextConfiguration, declaringClass.getName())); - } - - ContextConfigurationAttributes attributes = new ContextConfigurationAttributes(declaringClass, - contextConfiguration); - if (logger.isTraceEnabled()) { - logger.trace("Resolved context configuration attributes: " + attributes); - } - attributesList.add(attributes); - } - - /** - * Convenience method for creating a {@link ContextConfigurationAttributes} - * instance from the supplied {@link AnnotationAttributes} and declaring - * class and then adding the attributes to the supplied list. - * @since 4.0 - */ - private static void convertAnnotationAttributesToConfigAttributesAndAddToList(AnnotationAttributes annAttrs, - Class declaringClass, final List attributesList) { - if (logger.isTraceEnabled()) { - logger.trace(String.format("Retrieved @ContextConfiguration attributes [%s] for declaring class [%s].", - annAttrs, declaringClass.getName())); - } - - ContextConfigurationAttributes attributes = new ContextConfigurationAttributes(declaringClass, annAttrs); - if (logger.isTraceEnabled()) { - logger.trace("Resolved context configuration attributes: " + attributes); - } - attributesList.add(attributes); - } - - /** - * Resolve the list of lists of {@linkplain ContextConfigurationAttributes context - * configuration attributes} for the supplied {@linkplain Class test class} and its - * superclasses, taking into account context hierarchies declared via - * {@link ContextHierarchy @ContextHierarchy} and - * {@link ContextConfiguration @ContextConfiguration}. - * - *

The outer list represents a top-down ordering of context configuration - * attributes, where each element in the list represents the context configuration - * declared on a given test class in the class hierarchy. Each nested list - * contains the context configuration attributes declared either via a single - * instance of {@code @ContextConfiguration} on the particular class or via - * multiple instances of {@code @ContextConfiguration} declared within a - * single {@code @ContextHierarchy} instance on the particular class. - * Furthermore, each nested list maintains the order in which - * {@code @ContextConfiguration} instances are declared. - * - *

Note that the {@link ContextConfiguration#inheritLocations inheritLocations} and - * {@link ContextConfiguration#inheritInitializers() inheritInitializers} flags of - * {@link ContextConfiguration @ContextConfiguration} will not - * be taken into consideration. If these flags need to be honored, that must be - * handled manually when traversing the nested lists returned by this method. - * - * @param testClass the class for which to resolve the context hierarchy attributes - * (must not be {@code null}) - * @return the list of lists of configuration attributes for the specified class; - * never {@code null} - * @throws IllegalArgumentException if the supplied class is {@code null}; if - * neither {@code @ContextConfiguration} nor {@code @ContextHierarchy} is - * present on the supplied class; or if a test class or composed annotation - * in the class hierarchy declares both {@code @ContextConfiguration} and - * {@code @ContextHierarchy} as top-level annotations. - * @throws IllegalStateException if no class in the class hierarchy declares - * {@code @ContextHierarchy}. - * - * @since 3.2.2 - * @see #buildContextHierarchyMap(Class) - * @see #resolveContextConfigurationAttributes(Class) - */ - @SuppressWarnings("unchecked") - static List> resolveContextHierarchyAttributes(Class testClass) { - Assert.notNull(testClass, "Class must not be null"); - Assert.state(findAnnotation(testClass, ContextHierarchy.class) != null, "@ContextHierarchy must be present"); - - final Class contextConfigType = ContextConfiguration.class; - final Class contextHierarchyType = ContextHierarchy.class; - final List> hierarchyAttributes = new ArrayList>(); - - UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(testClass, contextConfigType, - contextHierarchyType); - Assert.notNull(descriptor, String.format( - "Could not find an 'annotation declaring class' for annotation type [%s] or [%s] and test class [%s]", - contextConfigType.getName(), contextHierarchyType.getName(), testClass.getName())); - - while (descriptor != null) { - Class rootDeclaringClass = descriptor.getRootDeclaringClass(); - Class declaringClass = descriptor.getDeclaringClass(); - - boolean contextConfigDeclaredLocally = isAnnotationDeclaredLocally(contextConfigType, declaringClass); - boolean contextHierarchyDeclaredLocally = isAnnotationDeclaredLocally(contextHierarchyType, declaringClass); - - if (contextConfigDeclaredLocally && contextHierarchyDeclaredLocally) { - String msg = String.format("Class [%s] has been configured with both @ContextConfiguration " - + "and @ContextHierarchy. Only one of these annotations may be declared on a test class " - + "or composed annotation.", declaringClass.getName()); - logger.error(msg); - throw new IllegalStateException(msg); - } - - final List configAttributesList = new ArrayList(); - - if (contextConfigDeclaredLocally) { - convertAnnotationAttributesToConfigAttributesAndAddToList(descriptor.getAnnotationAttributes(), - rootDeclaringClass, configAttributesList); - } - else if (contextHierarchyDeclaredLocally) { - ContextHierarchy contextHierarchy = getAnnotation(declaringClass, contextHierarchyType); - for (ContextConfiguration contextConfiguration : contextHierarchy.value()) { - convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, rootDeclaringClass, - configAttributesList); - } - } - else { - // This should theoretically never happen... - String msg = String.format("Test class [%s] has been configured with neither @ContextConfiguration " - + "nor @ContextHierarchy as a class-level annotation.", rootDeclaringClass.getName()); - logger.error(msg); - throw new IllegalStateException(msg); - } - - hierarchyAttributes.add(0, configAttributesList); - - descriptor = findAnnotationDescriptorForTypes(rootDeclaringClass.getSuperclass(), contextConfigType, - contextHierarchyType); - } - - return hierarchyAttributes; - } - - /** - * Build a context hierarchy map for the supplied {@linkplain Class - * test class} and its superclasses, taking into account context hierarchies - * declared via {@link ContextHierarchy @ContextHierarchy} and - * {@link ContextConfiguration @ContextConfiguration}. - * - *

Each value in the map represents the consolidated list of {@linkplain - * ContextConfigurationAttributes context configuration attributes} for a - * given level in the context hierarchy (potentially across the test class - * hierarchy), keyed by the {@link ContextConfiguration#name() name} of the - * context hierarchy level. - * - *

If a given level in the context hierarchy does not have an explicit - * name (i.e., configured via {@link ContextConfiguration#name}), a name will - * be generated for that hierarchy level by appending the numerical level to - * the {@link #GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX}. - * - * @param testClass the class for which to resolve the context hierarchy map - * (must not be {@code null}) - * @return a map of context configuration attributes for the context hierarchy, - * keyed by context hierarchy level name; never {@code null} - * @throws IllegalArgumentException if the lists of context configuration - * attributes for each level in the {@code @ContextHierarchy} do not define - * unique context configuration within the overall hierarchy. - * - * @since 3.2.2 - * @see #resolveContextHierarchyAttributes(Class) - */ - static Map> buildContextHierarchyMap(Class testClass) { - final Map> map = new LinkedHashMap>(); - int hierarchyLevel = 1; - - for (List configAttributesList : resolveContextHierarchyAttributes(testClass)) { - for (ContextConfigurationAttributes configAttributes : configAttributesList) { - String name = configAttributes.getName(); - - // Assign a generated name? - if (!StringUtils.hasText(name)) { - name = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + hierarchyLevel; - } - - // Encountered a new context hierarchy level? - if (!map.containsKey(name)) { - hierarchyLevel++; - map.put(name, new ArrayList()); - } - - map.get(name).add(configAttributes); - } - } - - // Check for uniqueness - Set> set = new HashSet>(map.values()); - if (set.size() != map.size()) { - String msg = String.format("The @ContextConfiguration elements configured via " - + "@ContextHierarchy in test class [%s] and its superclasses must " - + "define unique contexts per hierarchy level.", testClass.getName()); - logger.error(msg); - throw new IllegalStateException(msg); - } - - return map; - } - - /** - * Resolve the list of {@linkplain ContextConfigurationAttributes context - * configuration attributes} for the supplied {@linkplain Class test class} and its - * superclasses. - * - *

Note that the {@link ContextConfiguration#inheritLocations inheritLocations} and - * {@link ContextConfiguration#inheritInitializers() inheritInitializers} flags of - * {@link ContextConfiguration @ContextConfiguration} will not - * be taken into consideration. If these flags need to be honored, that must be - * handled manually when traversing the list returned by this method. - * - * @param testClass the class for which to resolve the configuration attributes (must - * not be {@code null}) - * @return the list of configuration attributes for the specified class, ordered - * bottom-up (i.e., as if we were traversing up the class hierarchy); - * never {@code null} - * @throws IllegalArgumentException if the supplied class is {@code null} or if - * {@code @ContextConfiguration} is not present on the supplied class - */ - static List resolveContextConfigurationAttributes(Class testClass) { - Assert.notNull(testClass, "Class must not be null"); - - final List attributesList = new ArrayList(); - - Class annotationType = ContextConfiguration.class; - - AnnotationDescriptor descriptor = findAnnotationDescriptor(testClass, annotationType); - Assert.notNull(descriptor, String.format( - "Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]", - annotationType.getName(), testClass.getName())); - - while (descriptor != null) { - convertAnnotationAttributesToConfigAttributesAndAddToList(descriptor.getAnnotationAttributes(), - descriptor.getRootDeclaringClass(), attributesList); - descriptor = findAnnotationDescriptor(descriptor.getRootDeclaringClass().getSuperclass(), annotationType); - } - - return attributesList; - } - - /** - * Resolve the list of merged {@code ApplicationContextInitializer} classes for the - * supplied list of {@code ContextConfigurationAttributes}. - * - *

Note that the {@link ContextConfiguration#inheritInitializers inheritInitializers} - * flag of {@link ContextConfiguration @ContextConfiguration} will be taken into - * consideration. Specifically, if the {@code inheritInitializers} flag is set to - * {@code true} for a given level in the class hierarchy represented by the provided - * configuration attributes, context initializer classes defined at the given level - * will be merged with those defined in higher levels of the class hierarchy. - * - * @param configAttributesList the list of configuration attributes to process; must - * not be {@code null} or empty; must be ordered bottom-up - * (i.e., as if we were traversing up the class hierarchy) - * @return the set of merged context initializer classes, including those from - * superclasses if appropriate (never {@code null}) - * @since 3.2 - */ - static Set>> resolveInitializerClasses( - List configAttributesList) { - Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty"); - - final Set>> initializerClasses = // - new HashSet>>(); - - for (ContextConfigurationAttributes configAttributes : configAttributesList) { - if (logger.isTraceEnabled()) { - logger.trace(String.format("Processing context initializers for context configuration attributes %s", - configAttributes)); - } - - initializerClasses.addAll(Arrays.asList(configAttributes.getInitializers())); - - if (!configAttributes.isInheritInitializers()) { - break; - } - } - - return initializerClasses; - } - - /** - * Resolve active bean definition profiles for the supplied {@link Class}. - * - *

Note that the {@link ActiveProfiles#inheritProfiles inheritProfiles} flag of - * {@link ActiveProfiles @ActiveProfiles} will be taken into consideration. - * Specifically, if the {@code inheritProfiles} flag is set to {@code true}, profiles - * defined in the test class will be merged with those defined in superclasses. - * - * @param testClass the class for which to resolve the active profiles (must not be - * {@code null}) - * @return the set of active profiles for the specified class, including active - * profiles from superclasses if appropriate (never {@code null}) - * @see ActiveProfiles - * @see org.springframework.context.annotation.Profile - */ - static String[] resolveActiveProfiles(Class testClass) { - Assert.notNull(testClass, "Class must not be null"); - - Class annotationType = ActiveProfiles.class; - - AnnotationDescriptor descriptor = findAnnotationDescriptor(testClass, annotationType); - - if (descriptor == null && logger.isDebugEnabled()) { - logger.debug(String.format( - "Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]", - annotationType.getName(), testClass.getName())); - } - - final Set activeProfiles = new HashSet(); - - while (descriptor != null) { - Class rootDeclaringClass = descriptor.getRootDeclaringClass(); - Class declaringClass = descriptor.getDeclaringClass(); - - AnnotationAttributes annAttrs = descriptor.getAnnotationAttributes(); - if (logger.isTraceEnabled()) { - logger.trace(String.format("Retrieved @ActiveProfiles attributes [%s] for declaring class [%s].", - annAttrs, declaringClass.getName())); - } - validateActiveProfilesConfiguration(declaringClass, annAttrs); - - String[] profiles = annAttrs.getStringArray("profiles"); - String[] valueProfiles = annAttrs.getStringArray("value"); - Class resolverClass = annAttrs.getClass("resolver"); - - boolean resolverDeclared = !ActiveProfilesResolver.class.equals(resolverClass); - boolean valueDeclared = !ObjectUtils.isEmpty(valueProfiles); - - if (resolverDeclared) { - ActiveProfilesResolver resolver = null; - try { - resolver = instantiateClass(resolverClass, ActiveProfilesResolver.class); - } - catch (Exception e) { - String msg = String.format("Could not instantiate ActiveProfilesResolver of " - + "type [%s] for test class [%s].", resolverClass.getName(), rootDeclaringClass.getName()); - logger.error(msg); - throw new IllegalStateException(msg, e); - } - - profiles = resolver.resolve(rootDeclaringClass); - if (profiles == null) { - String msg = String.format( - "ActiveProfilesResolver [%s] returned a null array of bean definition profiles.", - resolverClass.getName()); - logger.error(msg); - throw new IllegalStateException(msg); - } - } - else if (valueDeclared) { - profiles = valueProfiles; - } - - for (String profile : profiles) { - if (StringUtils.hasText(profile)) { - activeProfiles.add(profile.trim()); - } - } - - descriptor = annAttrs.getBoolean("inheritProfiles") ? findAnnotationDescriptor( - rootDeclaringClass.getSuperclass(), annotationType) : null; - } - - return StringUtils.toStringArray(activeProfiles); - } - - private static void validateActiveProfilesConfiguration(Class declaringClass, AnnotationAttributes annAttrs) { - String[] valueProfiles = annAttrs.getStringArray("value"); - String[] profiles = annAttrs.getStringArray("profiles"); - Class resolverClass = annAttrs.getClass("resolver"); - boolean valueDeclared = !ObjectUtils.isEmpty(valueProfiles); - boolean profilesDeclared = !ObjectUtils.isEmpty(profiles); - boolean resolverDeclared = !ActiveProfilesResolver.class.equals(resolverClass); - - String msg = null; - - if (valueDeclared && profilesDeclared) { - msg = String.format("Test class [%s] has been configured with @ActiveProfiles' 'value' [%s] " - + "and 'profiles' [%s] attributes. Only one declaration of active bean " - + "definition profiles is permitted per @ActiveProfiles annotation.", declaringClass.getName(), - ObjectUtils.nullSafeToString(valueProfiles), ObjectUtils.nullSafeToString(profiles)); - } - else if (valueDeclared && resolverDeclared) { - msg = String.format("Test class [%s] has been configured with @ActiveProfiles' 'value' [%s] " - + "and 'resolver' [%s] attributes. Only one source of active bean " - + "definition profiles is permitted per @ActiveProfiles annotation, " - + "either declaritively or programmatically.", declaringClass.getName(), - ObjectUtils.nullSafeToString(valueProfiles), resolverClass.getName()); - } - else if (profilesDeclared && resolverDeclared) { - msg = String.format("Test class [%s] has been configured with @ActiveProfiles' 'profiles' [%s] " - + "and 'resolver' [%s] attributes. Only one source of active bean " - + "definition profiles is permitted per @ActiveProfiles annotation, " - + "either declaritively or programmatically.", declaringClass.getName(), - ObjectUtils.nullSafeToString(profiles), resolverClass.getName()); - } - - if (msg != null) { - logger.error(msg); - throw new IllegalStateException(msg); - } - } - - /** - * Build the {@link MergedContextConfiguration merged context configuration} for - * the supplied {@link Class testClass} and {@code defaultContextLoaderClassName}, - * taking into account context hierarchies declared via - * {@link ContextHierarchy @ContextHierarchy} and - * {@link ContextConfiguration @ContextConfiguration}. - * @param testClass the test class for which the {@code MergedContextConfiguration} - * should be built (must not be {@code null}) - * @param defaultContextLoaderClassName the name of the default {@code ContextLoader} - * class to use (may be {@code null}) - * @param cacheAwareContextLoaderDelegate the cache-aware context loader delegate to - * be passed to the {@code MergedContextConfiguration} constructor - * @return the merged context configuration - * @see #buildContextHierarchyMap(Class) - * @see #buildMergedContextConfiguration(Class, List, String, MergedContextConfiguration, CacheAwareContextLoaderDelegate) - */ - @SuppressWarnings({ "unchecked" }) - static MergedContextConfiguration buildMergedContextConfiguration(Class testClass, - String defaultContextLoaderClassName, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { - - if (findAnnotationDescriptorForTypes(testClass, ContextConfiguration.class, ContextHierarchy.class) == null) { - if (logger.isInfoEnabled()) { - logger.info(String.format( - "Neither @ContextConfiguration nor @ContextHierarchy found for test class [%s]", - testClass.getName())); - } - return new MergedContextConfiguration(testClass, null, null, null, null); - } - - if (findAnnotation(testClass, ContextHierarchy.class) != null) { - Map> hierarchyMap = buildContextHierarchyMap(testClass); - - MergedContextConfiguration parentConfig = null; - MergedContextConfiguration mergedConfig = null; - - for (List list : hierarchyMap.values()) { - List reversedList = new ArrayList(list); - Collections.reverse(reversedList); - - // Don't use the supplied testClass; instead ensure that we are - // building the MCC for the actual test class that declared the - // configuration for the current level in the context hierarchy. - Assert.notEmpty(reversedList, "ContextConfigurationAttributes list must not be empty"); - Class declaringClass = reversedList.get(0).getDeclaringClass(); - - mergedConfig = buildMergedContextConfiguration(declaringClass, reversedList, - defaultContextLoaderClassName, parentConfig, cacheAwareContextLoaderDelegate); - parentConfig = mergedConfig; - } - - // Return the last level in the context hierarchy - return mergedConfig; - } - else { - return buildMergedContextConfiguration(testClass, resolveContextConfigurationAttributes(testClass), - defaultContextLoaderClassName, null, cacheAwareContextLoaderDelegate); - } - } - - /** - * Build the {@link MergedContextConfiguration merged context configuration} for the - * supplied {@link Class testClass}, context configuration attributes, - * {@code defaultContextLoaderClassName}, and parent context configuration. - * - * @param testClass the test class for which the {@code MergedContextConfiguration} - * should be built (must not be {@code null}) - * @param configAttributesList the list of context configuration attributes for the - * specified test class, ordered bottom-up (i.e., as if we were - * traversing up the class hierarchy); never {@code null} or empty - * @param defaultContextLoaderClassName the name of the default {@code ContextLoader} - * class to use (may be {@code null}) - * @param parentConfig the merged context configuration for the parent application - * context in a context hierarchy, or {@code null} if there is no parent - * @param cacheAwareContextLoaderDelegate the cache-aware context loader delegate to - * be passed to the {@code MergedContextConfiguration} constructor - * @return the merged context configuration - * @see #resolveContextLoader - * @see #resolveContextConfigurationAttributes - * @see SmartContextLoader#processContextConfiguration - * @see ContextLoader#processLocations - * @see #resolveActiveProfiles - * @see MergedContextConfiguration - */ - private static MergedContextConfiguration buildMergedContextConfiguration(final Class testClass, - final List configAttributesList, - final String defaultContextLoaderClassName, MergedContextConfiguration parentConfig, - CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { - - final ContextLoader contextLoader = resolveContextLoader(testClass, configAttributesList, - defaultContextLoaderClassName); - final List locationsList = new ArrayList(); - final List> classesList = new ArrayList>(); - - for (ContextConfigurationAttributes configAttributes : configAttributesList) { - if (logger.isTraceEnabled()) { - logger.trace(String.format("Processing locations and classes for context configuration attributes %s", - configAttributes)); - } - - if (contextLoader instanceof SmartContextLoader) { - SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader; - smartContextLoader.processContextConfiguration(configAttributes); - locationsList.addAll(0, Arrays.asList(configAttributes.getLocations())); - classesList.addAll(0, Arrays.asList(configAttributes.getClasses())); - } - else { - String[] processedLocations = contextLoader.processLocations(configAttributes.getDeclaringClass(), - configAttributes.getLocations()); - locationsList.addAll(0, Arrays.asList(processedLocations)); - // Legacy ContextLoaders don't know how to process classes - } - - if (!configAttributes.isInheritLocations()) { - break; - } - } - - String[] locations = StringUtils.toStringArray(locationsList); - Class[] classes = ClassUtils.toClassArray(classesList); - Set>> initializerClasses = resolveInitializerClasses(configAttributesList); - String[] activeProfiles = resolveActiveProfiles(testClass); - - MergedContextConfiguration mergedConfig = buildWebMergedContextConfiguration(testClass, locations, classes, - initializerClasses, activeProfiles, contextLoader, cacheAwareContextLoaderDelegate, parentConfig); - - if (mergedConfig == null) { - mergedConfig = new MergedContextConfiguration(testClass, locations, classes, initializerClasses, - activeProfiles, contextLoader, cacheAwareContextLoaderDelegate, parentConfig); - } - - return mergedConfig; - } - - /** - * Load the {@link org.springframework.test.context.web.WebAppConfiguration} - * class, using reflection in order to avoid package cycles. - * - * @return the {@code @WebAppConfiguration} class or {@code null} if it cannot be loaded - * @since 3.2 - */ - @SuppressWarnings("unchecked") - private static Class loadWebAppConfigurationClass() { - Class webAppConfigClass = null; - try { - webAppConfigClass = (Class) ClassUtils.forName(WEB_APP_CONFIGURATION_CLASS_NAME, - ContextLoaderUtils.class.getClassLoader()); - } - catch (Throwable t) { - if (logger.isDebugEnabled()) { - logger.debug("Could not load @WebAppConfiguration class [" + WEB_APP_CONFIGURATION_CLASS_NAME + "].", t); - } - } - return webAppConfigClass; - } - - /** - * Attempt to build a {@link org.springframework.test.context.web.WebMergedContextConfiguration} - * from the supplied arguments, using reflection in order to avoid package cycles. - * - * @return the {@code WebMergedContextConfiguration} or {@code null} if it could not be built - * @since 3.2 - */ - @SuppressWarnings("unchecked") - private static MergedContextConfiguration buildWebMergedContextConfiguration( - Class testClass, - String[] locations, - Class[] classes, - Set>> initializerClasses, - String[] activeProfiles, ContextLoader contextLoader, - CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parentConfig) { - - Class webAppConfigClass = loadWebAppConfigurationClass(); - if (webAppConfigClass != null) { - - Annotation annotation = findAnnotation(testClass, webAppConfigClass); - if (annotation != null) { - - String resourceBasePath = (String) AnnotationUtils.getValue(annotation); - - try { - Class webMergedConfigClass = (Class) ClassUtils.forName( - WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME, ContextLoaderUtils.class.getClassLoader()); - - Constructor constructor = ClassUtils.getConstructorIfAvailable( - webMergedConfigClass, Class.class, String[].class, Class[].class, Set.class, String[].class, - String.class, ContextLoader.class, CacheAwareContextLoaderDelegate.class, - MergedContextConfiguration.class); - - if (constructor != null) { - return instantiateClass(constructor, testClass, locations, classes, initializerClasses, - activeProfiles, resourceBasePath, contextLoader, cacheAwareContextLoaderDelegate, - parentConfig); - } - } - catch (Throwable t) { - if (logger.isDebugEnabled()) { - logger.debug("Could not instantiate [" + WEB_MERGED_CONTEXT_CONFIGURATION_CLASS_NAME + "].", t); - } - } - } - } - - return null; - } - -} diff --git a/spring-test/src/main/java/org/springframework/test/context/DefaultBootstrapContext.java b/spring-test/src/main/java/org/springframework/test/context/DefaultBootstrapContext.java new file mode 100644 index 00000000000..c0f43571584 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/DefaultBootstrapContext.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2014 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; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; + +/** + * Default implementation of the {@link BootstrapContext} interface. + * + * @author Sam Brannen + * @since 4.1 + */ +class DefaultBootstrapContext implements BootstrapContext { + + private final Class testClass; + private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate; + + + 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; + this.cacheAwareContextLoaderDelegate = cacheAwareContextLoaderDelegate; + } + + /** + * {@inheritDoc} + */ + @Override + public Class getTestClass() { + return this.testClass; + } + + /** + * {@inheritDoc} + */ + @Override + public CacheAwareContextLoaderDelegate getCacheAwareContextLoaderDelegate() { + return this.cacheAwareContextLoaderDelegate; + } + + /** + * Provide a String representation of this bootstrap context's state. + */ + @Override + public String toString() { + return new ToStringCreator(this)// + .append("testClass", testClass)// + .append("cacheAwareContextLoaderDelegate", nullSafeToString(cacheAwareContextLoaderDelegate))// + .toString(); + } + + /** + * Generate a null-safe {@link String} representation of the supplied + * {@link CacheAwareContextLoaderDelegate} based solely on the fully qualified + * name of the delegate or "null" if the supplied delegate is + * {@code null}. + */ + private static String nullSafeToString(CacheAwareContextLoaderDelegate delegate) { + return delegate == null ? "null" : delegate.getClass().getName(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java new file mode 100644 index 00000000000..2d1c2e0d9a6 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/DefaultCacheAwareContextLoaderDelegate.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2014 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; + +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.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. + * + * @author Sam Brannen + * @since 4.1 + */ +class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContextLoaderDelegate { + + private static final Log logger = LogFactory.getLog(DefaultCacheAwareContextLoaderDelegate.class); + + private final ContextCache contextCache; + + + DefaultCacheAwareContextLoaderDelegate(ContextCache contextCache) { + Assert.notNull(contextCache, "ContextCache must not be null"); + this.contextCache = 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) + throws Exception { + ContextLoader contextLoader = mergedContextConfiguration.getContextLoader(); + Assert.notNull(contextLoader, "Cannot load an ApplicationContext with a NULL 'contextLoader'. " + + "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy."); + + ApplicationContext applicationContext; + + if (contextLoader instanceof SmartContextLoader) { + SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader; + applicationContext = smartContextLoader.loadContext(mergedContextConfiguration); + } + else { + String[] locations = mergedContextConfiguration.getLocations(); + Assert.notNull(locations, "Cannot load an ApplicationContext with a NULL 'locations' array. " + + "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy."); + applicationContext = contextLoader.loadContext(locations); + } + + return applicationContext; + } + + /** + * {@inheritDoc} + */ + public ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration) { + synchronized (contextCache) { + ApplicationContext context = contextCache.get(mergedContextConfiguration); + if (context == null) { + try { + context = loadContextInternal(mergedContextConfiguration); + if (logger.isDebugEnabled()) { + logger.debug(String.format("Storing ApplicationContext in cache under key [%s].", + mergedContextConfiguration)); + } + contextCache.put(mergedContextConfiguration, context); + } + catch (Exception ex) { + throw new IllegalStateException("Failed to load ApplicationContext", ex); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Retrieved ApplicationContext from cache with key [%s].", + mergedContextConfiguration)); + } + } + return context; + } + } + + /** + * {@inheritDoc} + */ + @Override + public void closeContext(MergedContextConfiguration mergedContextConfiguration, HierarchyMode hierarchyMode) { + contextCache.remove(mergedContextConfiguration, hierarchyMode); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java b/spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java index 22d6580a3df..b3fcfc13826 100644 --- a/spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/DefaultTestContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -40,8 +40,6 @@ class DefaultTestContext extends AttributeAccessorSupport implements TestContext private static final long serialVersionUID = -5827157174866681233L; - private final ContextCache contextCache; - private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate; private final MergedContextConfiguration mergedContextConfiguration; @@ -56,49 +54,31 @@ class DefaultTestContext extends AttributeAccessorSupport implements TestContext /** - * Delegates to {@link #DefaultTestContext(Class, ContextCache, String)} with a - * value of {@code null} for the default {@code ContextLoader} class name. + * 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}) */ - DefaultTestContext(Class testClass, ContextCache contextCache) { - this(testClass, contextCache, 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(); } /** - * Construct a new test context for the supplied {@linkplain Class test class} - * and {@linkplain ContextCache context cache} and parse the corresponding - * {@link ContextConfiguration @ContextConfiguration} or - * {@link ContextHierarchy @ContextHierarchy} annotation, if present. - *

If the supplied class name for the default {@code ContextLoader} - * is {@code null} or empty and no concrete {@code ContextLoader} - * class is explicitly supplied via {@code @ContextConfiguration}, a - * {@link org.springframework.test.context.support.DelegatingSmartContextLoader - * DelegatingSmartContextLoader} or - * {@link org.springframework.test.context.web.WebDelegatingSmartContextLoader - * WebDelegatingSmartContextLoader} will be used instead. - * @param testClass the test class for which the test context should be - * constructed (must not be {@code null}) - * @param contextCache the context cache from which the constructed test - * context should retrieve application contexts (must not be - * {@code null}) - * @param defaultContextLoaderClassName the name of the default - * {@code ContextLoader} class to use (may be {@code null}) + * {@inheritDoc} */ - DefaultTestContext(Class testClass, ContextCache contextCache, String defaultContextLoaderClassName) { - Assert.notNull(testClass, "Test class must not be null"); - Assert.notNull(contextCache, "ContextCache must not be null"); - - this.testClass = testClass; - this.contextCache = contextCache; - this.cacheAwareContextLoaderDelegate = new CacheAwareContextLoaderDelegate(contextCache); - this.mergedContextConfiguration = ContextLoaderUtils.buildMergedContextConfiguration(testClass, - defaultContextLoaderClassName, cacheAwareContextLoaderDelegate); + public ApplicationContext getApplicationContext() { + return cacheAwareContextLoaderDelegate.loadContext(mergedContextConfiguration); } /** * {@inheritDoc} */ - public ApplicationContext getApplicationContext() { - return cacheAwareContextLoaderDelegate.loadContext(mergedContextConfiguration); + public void markApplicationContextDirty(HierarchyMode hierarchyMode) { + cacheAwareContextLoaderDelegate.closeContext(mergedContextConfiguration, hierarchyMode); } /** @@ -129,13 +109,6 @@ class DefaultTestContext extends AttributeAccessorSupport implements TestContext return testException; } - /** - * {@inheritDoc} - */ - public void markApplicationContextDirty(HierarchyMode hierarchyMode) { - contextCache.remove(mergedContextConfiguration, hierarchyMode); - } - /** * {@inheritDoc} */ 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 new file mode 100644 index 00000000000..5e22e497e2a --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2014 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; + +import java.util.List; + +/** + * {@code TestContextBootstrapper} defines a strategy 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 + * {@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 + * it manages. + * + *

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

Note: this SPI might potentially 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 + * {@link org.springframework.test.context.support.AbstractTestContextBootstrapper + * AbstractTestContextBootstrapper} or one of its concrete subclasses instead. + * + * @author Sam Brannen + * @since 4.1 + * @see BootstrapWith + * @see BootstrapContext + */ +public interface TestContextBootstrapper { + + /** + * Set the {@link BootstrapContext} to be used by this bootstrapper. + */ + void setBootstrapContext(BootstrapContext bootstrapContext); + + /** + * Get the {@link BootstrapContext} associated with this bootstrapper. + */ + 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. Concrete implementations + * are free to determine what comprises the set of default listeners. + *

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(); + + /** + * Build the {@linkplain MergedContextConfiguration merged context configuration} + * for the test class in the {@link BootstrapContext} associated with this + * bootstrapper. + *

Implementations must take the following into account when building the + * merged configuration: + *

    + *
  • Context hierarchies declared via {@link ContextHierarchy @ContextHierarchy} + * and {@link ContextConfiguration @ContextConfiguration}
  • + *
  • Active bean definition profiles declared via {@link ActiveProfiles @ActiveProfiles}
  • + *
  • {@linkplain org.springframework.context.ApplicationContextInitializer + * Context initializers} declared via {@link ContextConfiguration#initializers}
  • + *

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

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

      + *
    1. If a {@code ContextLoader} class has been explicitly declared via + * {@link ContextConfiguration#loader}, use it.
    2. + *
    3. Otherwise, if the name of a {@linkplain BootstrapContext#getCustomDefaultContextLoaderClassName + * custom default ContextLoader class} has been provided in the {@link BootstrapContext}, + * use it.
    4. + *
    5. Otherwise, concrete implementations are free to determine which + * {@code ContextLoader} class to use as as default.
    6. + * @return the merged context configuration, never {@code null} + */ + MergedContextConfiguration buildMergedContextConfiguration(); + +} 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 8255db3b27e..75e22ab16c9 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-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -18,119 +18,111 @@ package org.springframework.test.context; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.beans.BeanUtils; -import org.springframework.context.ApplicationContext; -import org.springframework.core.annotation.AnnotationAttributes; -import org.springframework.test.context.MetaAnnotationUtils.AnnotationDescriptor; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; - -import static org.springframework.test.context.MetaAnnotationUtils.*; /** - *

      - * {@code TestContextManager} is the main entry point into the - * Spring TestContext Framework, which provides support for loading and - * accessing {@link ApplicationContext application contexts}, dependency - * injection of test instances, - * {@link org.springframework.transaction.annotation.Transactional - * transactional} execution of test methods, etc. - *

      - *

      - * Specifically, a {@code TestContextManager} is responsible for managing a + * {@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. + * + *

      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 * execution points: - *

      + * *
        *
      • {@link #beforeTestClass() before test class execution}: prior to any * before class methods of a particular testing framework (e.g., JUnit - * 4's {@link org.junit.BeforeClass @BeforeClass})
      • + * 4's {@link org.junit.BeforeClass @BeforeClass}) *
      • {@link #prepareTestInstance(Object) test instance preparation}: * immediately following instantiation of the test instance
      • *
      • {@link #beforeTestMethod(Object, Method) before test method execution}: * prior to any before methods of a particular testing framework (e.g., - * JUnit 4's {@link org.junit.Before @Before})
      • + * JUnit 4's {@link org.junit.Before @Before}) *
      • {@link #afterTestMethod(Object, Method, Throwable) after test method * execution}: after any after methods of a particular testing - * framework (e.g., JUnit 4's {@link org.junit.After @After})
      • + * framework (e.g., JUnit 4's {@link org.junit.After @After}) *
      • {@link #afterTestClass() after test class execution}: after any * after class methods of a particular testing framework (e.g., JUnit - * 4's {@link org.junit.AfterClass @AfterClass})
      • + * 4's {@link org.junit.AfterClass @AfterClass}) *
      * * @author Sam Brannen * @author Juergen Hoeller * @since 2.5 + * @see BootstrapWith + * @see BootstrapContext + * @see TestContextBootstrapper * @see TestContext + * @see TestExecutionListener * @see TestExecutionListeners * @see ContextConfiguration + * @see ContextHierarchy * @see org.springframework.test.context.transaction.TransactionConfiguration */ public class TestContextManager { - private static final String[] DEFAULT_TEST_EXECUTION_LISTENER_CLASS_NAMES = new String[] { - "org.springframework.test.context.web.ServletTestExecutionListener", - "org.springframework.test.context.support.DependencyInjectionTestExecutionListener", - "org.springframework.test.context.support.DirtiesContextTestExecutionListener", - "org.springframework.test.context.transaction.TransactionalTestExecutionListener" }; - private static final Log logger = LogFactory.getLog(TestContextManager.class); /** - * Cache of Spring application contexts. This needs to be static, as tests - * may be destroyed and recreated between running individual test methods, - * for example with JUnit. + * 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(); /** - * Delegates to {@link #TestContextManager(Class, String)} with a value of - * {@code null} for the default {@code ContextLoader} class name. + * 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. + * @param testClass the test class to be managed + * @see #registerTestExecutionListeners(List) */ public TestContextManager(Class testClass) { - this(testClass, null); + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = new DefaultCacheAwareContextLoaderDelegate( + contextCache); + BootstrapContext bootstrapContext = new DefaultBootstrapContext(testClass, cacheAwareContextLoaderDelegate); + this.testContextBootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext); + this.testContext = new DefaultTestContext(testContextBootstrapper); + registerTestExecutionListeners(testContextBootstrapper.getTestExecutionListeners()); } /** - * Constructs a new {@code TestContextManager} for the specified {@linkplain Class - * test class} and automatically {@link #registerTestExecutionListeners registers} the - * {@link TestExecutionListener TestExecutionListeners} configured for the test class - * via the {@link TestExecutionListeners @TestExecutionListeners} annotation. - * @param testClass the test class to be managed - * @param defaultContextLoaderClassName the name of the default {@code ContextLoader} - * class to use (may be {@code null}) - * @see #registerTestExecutionListeners(TestExecutionListener...) + * Get the {@link TestContext} managed by this {@code TestContextManager}. */ - public TestContextManager(Class testClass, String defaultContextLoaderClassName) { - this.testContext = new DefaultTestContext(testClass, contextCache, defaultContextLoaderClassName); - registerTestExecutionListeners(retrieveTestExecutionListeners(testClass)); + protected final TestContext getTestContext() { + return this.testContext; } /** - * Returns the {@link TestContext} managed by this - * {@code TestContextManager}. + * Register the supplied list of {@link TestExecutionListener TestExecutionListeners} + * by appending them to the list of listeners used by this {@code TestContextManager}. + * @see #registerTestExecutionListeners(TestExecutionListener...) */ - protected final TestContext getTestContext() { - return this.testContext; + public void registerTestExecutionListeners(List testExecutionListeners) { + registerTestExecutionListeners(testExecutionListeners.toArray(new TestExecutionListener[testExecutionListeners.size()])); } /** - * Register the supplied {@link TestExecutionListener TestExecutionListeners} - * by appending them to the set of listeners used by this {@code TestContextManager}. + * Register the supplied array of {@link TestExecutionListener TestExecutionListeners} + * by appending them to the list of listeners used by this {@code TestContextManager}. */ public void registerTestExecutionListeners(TestExecutionListener... testExecutionListeners) { for (TestExecutionListener listener : testExecutionListeners) { @@ -162,110 +154,11 @@ public class TestContextManager { return listenersReversed; } - /** - * Retrieve an array of newly instantiated {@link TestExecutionListener TestExecutionListeners} - * for the specified {@link Class class}. If {@link TestExecutionListeners @TestExecutionListeners} - * is not present on the supplied class, the default listeners will be returned. - *

      Note that the {@link TestExecutionListeners#inheritListeners() inheritListeners} flag of - * {@link TestExecutionListeners @TestExecutionListeners} will be taken into consideration. - * Specifically, if the {@code inheritListeners} flag is set to {@code true}, listeners - * defined in the annotated class will be appended to the listeners defined in superclasses. - * @param clazz the test class for which the listeners should be retrieved - * @return an array of TestExecutionListeners for the specified class - */ - @SuppressWarnings("unchecked") - private TestExecutionListener[] retrieveTestExecutionListeners(Class clazz) { - Assert.notNull(clazz, "Class must not be null"); - Class annotationType = TestExecutionListeners.class; - List> classesList = new ArrayList>(); - - AnnotationDescriptor descriptor = findAnnotationDescriptor(clazz, annotationType); - - // Use defaults? - if (descriptor == null) { - if (logger.isDebugEnabled()) { - logger.debug("@TestExecutionListeners is not present for class [" + clazz + "]: using defaults."); - } - classesList.addAll(getDefaultTestExecutionListenerClasses()); - } - else { - // Traverse the class hierarchy... - while (descriptor != null) { - Class declaringClass = descriptor.getDeclaringClass(); - - AnnotationAttributes annAttrs = descriptor.getAnnotationAttributes(); - if (logger.isTraceEnabled()) { - logger.trace(String.format( - "Retrieved @TestExecutionListeners attributes [%s] for declaring class [%s].", annAttrs, - declaringClass)); - } - - Class[] valueListenerClasses = (Class[]) annAttrs.getClassArray("value"); - Class[] listenerClasses = (Class[]) annAttrs.getClassArray("listeners"); - if (!ObjectUtils.isEmpty(valueListenerClasses) && !ObjectUtils.isEmpty(listenerClasses)) { - String msg = String.format( - "Class [%s] has been configured with @TestExecutionListeners' 'value' [%s] " - + "and 'listeners' [%s] attributes. Use one or the other, but not both.", - declaringClass, ObjectUtils.nullSafeToString(valueListenerClasses), - ObjectUtils.nullSafeToString(listenerClasses)); - logger.error(msg); - throw new IllegalStateException(msg); - } - else if (!ObjectUtils.isEmpty(valueListenerClasses)) { - listenerClasses = valueListenerClasses; - } - - if (listenerClasses != null) { - classesList.addAll(0, Arrays.> asList(listenerClasses)); - } - - descriptor = (annAttrs.getBoolean("inheritListeners") ? findAnnotationDescriptor( - descriptor.getRootDeclaringClass().getSuperclass(), annotationType) : null); - } - } - - List listeners = new ArrayList(classesList.size()); - for (Class listenerClass : classesList) { - try { - listeners.add(BeanUtils.instantiateClass(listenerClass)); - } - catch (NoClassDefFoundError err) { - if (logger.isInfoEnabled()) { - logger.info(String.format("Could not instantiate TestExecutionListener class [%s]. " - + "Specify custom listener classes or make the default listener classes " - + "(and their dependencies) available.", listenerClass.getName())); - } - } - } - return listeners.toArray(new TestExecutionListener[listeners.size()]); - } - - /** - * Determine the default {@link TestExecutionListener} classes. - */ - @SuppressWarnings("unchecked") - protected Set> getDefaultTestExecutionListenerClasses() { - Set> defaultListenerClasses = new LinkedHashSet>(); - for (String className : DEFAULT_TEST_EXECUTION_LISTENER_CLASS_NAMES) { - try { - defaultListenerClasses.add((Class) getClass().getClassLoader().loadClass( - className)); - } - catch (Throwable t) { - if (logger.isDebugEnabled()) { - logger.debug("Could not load default TestExecutionListener class [" + className - + "]. Specify custom listener classes or make the default listener classes available.", t); - } - } - } - return defaultListenerClasses; - } - /** * Hook for pre-processing a test class before execution of any * tests within the class. Should be called prior to any framework-specific * before class methods (e.g., methods annotated with JUnit's - * {@link org.junit.BeforeClass @BeforeClass}). + * {@link org.junit.BeforeClass @BeforeClass}). *

      An attempt will be made to give each registered * {@link TestExecutionListener} a chance to pre-process the test class * execution. If a listener throws an exception, however, the remaining @@ -331,7 +224,7 @@ public class TestContextManager { * {@link Method test method}, for example for setting up test fixtures, * starting a transaction, etc. Should be called prior to any * framework-specific before methods (e.g., methods annotated with - * JUnit's {@link org.junit.Before @Before}). + * JUnit's {@link org.junit.Before @Before}). *

      The managed {@link TestContext} will be updated with the supplied * {@code testInstance} and {@code testMethod}. *

      An attempt will be made to give each registered @@ -369,7 +262,7 @@ public class TestContextManager { * {@link Method test method}, for example for tearing down test fixtures, * ending a transaction, etc. Should be called after any framework-specific * after methods (e.g., methods annotated with JUnit's - * {@link org.junit.After @After}). + * {@link org.junit.After @After}). *

      The managed {@link TestContext} will be updated with the supplied * {@code testInstance}, {@code testMethod}, and * {@code exception}. @@ -421,7 +314,7 @@ public class TestContextManager { * Hook for post-processing a test class after execution of all * tests within the class. Should be called after any framework-specific * after class methods (e.g., methods annotated with JUnit's - * {@link org.junit.AfterClass @AfterClass}). + * {@link org.junit.AfterClass @AfterClass}). *

      Each registered {@link TestExecutionListener} will be given a chance to * post-process the test class. If a listener throws an exception, the * remaining registered listeners will still be called, but the first diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java index 30d8f43d30f..74464aa8d80 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -46,10 +46,21 @@ import java.lang.annotation.Target; @Target(ElementType.TYPE) public @interface TestExecutionListeners { + /** + * Alias for {@link #listeners() listeners}. + * + *

      This attribute may not be used in conjunction with + * {@link #listeners}, but it may be used instead of {@link #listeners}. + */ + Class[] value() default {}; + /** * The {@link TestExecutionListener TestExecutionListeners} to register with * a {@link TestContextManager}. * + *

      This attribute may not be used in conjunction with + * {@link #value}, but it may be used instead of {@link #value}. + * * @see org.springframework.test.context.web.ServletTestExecutionListener * @see org.springframework.test.context.support.DependencyInjectionTestExecutionListener * @see org.springframework.test.context.support.DirtiesContextTestExecutionListener @@ -57,11 +68,6 @@ public @interface TestExecutionListeners { */ Class[] listeners() default {}; - /** - * Alias for {@link #listeners() listeners}. - */ - Class[] value() default {}; - /** * Whether or not {@link #value() TestExecutionListeners} from superclasses * should be inherited. diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java index 4ec6101e422..d8633716c4f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java @@ -50,36 +50,32 @@ import org.springframework.test.context.junit4.statements.SpringRepeat; import org.springframework.util.ReflectionUtils; /** - *

      - * {@code SpringJUnit4ClassRunner} is a custom extension of + *

      {@code SpringJUnit4ClassRunner} is a custom extension of JUnit's * {@link BlockJUnit4ClassRunner} which provides functionality of the * Spring TestContext Framework to standard JUnit 4.5+ tests by means * of the {@link TestContextManager} and associated support classes and * annotations. - *

      - *

      - * The following list constitutes all annotations currently supported directly + * + *

      The following list constitutes all annotations currently supported directly * by {@code SpringJUnit4ClassRunner}. - * (Note that additional annotations may be supported by various - * {@link org.springframework.test.context.TestExecutionListener - * TestExecutionListeners}) - *

      + * (Note that additional annotations may be supported by various {@link + * org.springframework.test.context.TestExecutionListener TestExecutionListeners} + * or {@link org.springframework.test.context.TestContextBootstrapper + * TestContextBootstrapper} implementations) + * *
        *
      • {@link Test#expected() @Test(expected=...)}
      • *
      • {@link Test#timeout() @Test(timeout=...)}
      • *
      • {@link Timed @Timed}
      • *
      • {@link Repeat @Repeat}
      • *
      • {@link Ignore @Ignore}
      • - *
      • - * {@link org.springframework.test.annotation.ProfileValueSourceConfiguration + *
      • {@link org.springframework.test.annotation.ProfileValueSourceConfiguration * @ProfileValueSourceConfiguration}
      • - *
      • {@link org.springframework.test.annotation.IfProfileValue - * @IfProfileValue}
      • + *
      • {@link org.springframework.test.annotation.IfProfileValue @IfProfileValue}
      • *
      - *

      - * NOTE: As of Spring 3.0, {@code SpringJUnit4ClassRunner} requires - * JUnit 4.5+. - *

      + * + *

      NOTE: As of Spring 3.0, {@code SpringJUnit4ClassRunner} + * requires JUnit 4.5 or higher. * * @author Sam Brannen * @author Juergen Hoeller @@ -110,14 +106,12 @@ public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner { } /** - * Creates a new {@link TestContextManager} for the supplied test class and - * the configured default {@code ContextLoader} class name. - * Can be overridden by subclasses. + * Creates a new {@link TestContextManager} for the supplied test class. + *

      Can be overridden by subclasses. * @param clazz the test class to be managed - * @see #getDefaultContextLoaderClassName(Class) */ protected TestContextManager createTestContextManager(Class clazz) { - return new TestContextManager(clazz, getDefaultContextLoaderClassName(clazz)); + return new TestContextManager(clazz); } /** @@ -127,21 +121,6 @@ public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner { return this.testContextManager; } - /** - * Get the name of the default {@code ContextLoader} class to use for - * the supplied test class. The named class will be used if the test class - * does not explicitly declare a {@code ContextLoader} class via the - * {@code @ContextConfiguration} annotation. - *

      The default implementation returns {@code null}, thus implying use - * of the standard default {@code ContextLoader} class name. - * Can be overridden by subclasses. - * @param clazz the test class - * @return {@code null} - */ - protected String getDefaultContextLoaderClassName(Class clazz) { - return null; - } - /** * Returns a description suitable for an ignored test class if the test is * disabled via {@code @IfProfileValue} at the class-level, and 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 new file mode 100644 index 00000000000..0a6f472fec6 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java @@ -0,0 +1,435 @@ +/* + * Copyright 2002-2014 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.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.test.context.BootstrapContext; +import org.springframework.test.context.CacheAwareContextLoaderDelegate; +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.TestContextBootstrapper; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.util.MetaAnnotationUtils; +import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Abstract implementation of the {@link TestContextBootstrapper} interface which + * provides most of the behavior required by a bootstrapper. + * + *

      Concrete subclasses typically will only need to provide implementations for + * the following {@code abstract} methods: + *

        + *
      • {@link #getDefaultTestExecutionListenerClassNames()} + *
      • {@link #getDefaultContextLoaderClass(Class)} + *
      • {@link #buildMergedContextConfiguration(Class, String[], Class[], Set, String[], ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)} + * + * @author Sam Brannen + * @since 4.1 + */ +public abstract class AbstractTestContextBootstrapper implements TestContextBootstrapper { + + private static final Log logger = LogFactory.getLog(AbstractTestContextBootstrapper.class); + + private BootstrapContext bootstrapContext; + + + /** + * {@inheritDoc} + */ + @Override + public void setBootstrapContext(BootstrapContext bootstrapContext) { + this.bootstrapContext = bootstrapContext; + } + + /** + * {@inheritDoc} + */ + @Override + public BootstrapContext getBootstrapContext() { + return this.bootstrapContext; + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + @Override + public final List getTestExecutionListeners() { + Class clazz = getBootstrapContext().getTestClass(); + Class annotationType = TestExecutionListeners.class; + List> classesList = new ArrayList>(); + + AnnotationDescriptor descriptor = MetaAnnotationUtils.findAnnotationDescriptor(clazz, + annotationType); + + // Use defaults? + if (descriptor == null) { + if (logger.isDebugEnabled()) { + logger.debug("@TestExecutionListeners is not present for class [" + clazz + "]: using defaults."); + } + classesList.addAll(getDefaultTestExecutionListenerClasses()); + } + else { + // Traverse the class hierarchy... + while (descriptor != null) { + Class declaringClass = descriptor.getDeclaringClass(); + + AnnotationAttributes annAttrs = descriptor.getAnnotationAttributes(); + if (logger.isTraceEnabled()) { + logger.trace(String.format( + "Retrieved @TestExecutionListeners attributes [%s] for declaring class [%s].", annAttrs, + declaringClass)); + } + + Class[] valueListenerClasses = (Class[]) annAttrs.getClassArray("value"); + Class[] listenerClasses = (Class[]) annAttrs.getClassArray("listeners"); + if (!ObjectUtils.isEmpty(valueListenerClasses) && !ObjectUtils.isEmpty(listenerClasses)) { + String msg = String.format( + "Class [%s] has been configured with @TestExecutionListeners' 'value' [%s] " + + "and 'listeners' [%s] attributes. Use one or the other, but not both.", + declaringClass, ObjectUtils.nullSafeToString(valueListenerClasses), + ObjectUtils.nullSafeToString(listenerClasses)); + logger.error(msg); + throw new IllegalStateException(msg); + } + else if (!ObjectUtils.isEmpty(valueListenerClasses)) { + listenerClasses = valueListenerClasses; + } + + if (listenerClasses != null) { + classesList.addAll(0, Arrays.> asList(listenerClasses)); + } + + descriptor = (annAttrs.getBoolean("inheritListeners") ? MetaAnnotationUtils.findAnnotationDescriptor( + descriptor.getRootDeclaringClass().getSuperclass(), annotationType) : null); + } + } + + List listeners = new ArrayList(classesList.size()); + for (Class listenerClass : classesList) { + try { + listeners.add(BeanUtils.instantiateClass(listenerClass)); + } + catch (NoClassDefFoundError err) { + if (logger.isInfoEnabled()) { + logger.info(String.format("Could not instantiate TestExecutionListener [%s]. " + + "Specify custom listener classes or make the default listener classes " + + "(and their dependencies) available.", listenerClass.getName())); + } + } + } + return listeners; + } + + /** + * Get the default {@link TestExecutionListener} classes for this bootstrapper. + *

        This method is invoked by {@link #getTestExecutionListeners()} and + * delegates to {@link #getDefaultTestExecutionListenerClassNames()} to + * retrieve the class names. + *

        If a particular class cannot be loaded, a {@code DEBUG} message will + * be logged, but the associated exception will not be rethrown. + */ + @SuppressWarnings("unchecked") + protected Set> getDefaultTestExecutionListenerClasses() { + Set> defaultListenerClasses = new LinkedHashSet>(); + for (String className : getDefaultTestExecutionListenerClassNames()) { + try { + defaultListenerClasses.add((Class) getClass().getClassLoader().loadClass( + className)); + } + catch (Throwable t) { + if (logger.isDebugEnabled()) { + logger.debug("Could not load default TestExecutionListener class [" + className + + "]. Specify custom listener classes or make the default listener classes available.", t); + } + } + } + return defaultListenerClasses; + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings({ "unchecked" }) + @Override + public final MergedContextConfiguration buildMergedContextConfiguration() { + Class testClass = getBootstrapContext().getTestClass(); + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = getBootstrapContext().getCacheAwareContextLoaderDelegate(); + + if (MetaAnnotationUtils.findAnnotationDescriptorForTypes(testClass, ContextConfiguration.class, + ContextHierarchy.class) == null) { + if (logger.isInfoEnabled()) { + logger.info(String.format( + "Neither @ContextConfiguration nor @ContextHierarchy found for test class [%s]", + testClass.getName())); + } + return new MergedContextConfiguration(testClass, null, null, null, null); + } + + if (AnnotationUtils.findAnnotation(testClass, ContextHierarchy.class) != null) { + Map> hierarchyMap = ContextLoaderUtils.buildContextHierarchyMap(testClass); + + MergedContextConfiguration parentConfig = null; + MergedContextConfiguration mergedConfig = null; + + for (List list : hierarchyMap.values()) { + List reversedList = new ArrayList(list); + Collections.reverse(reversedList); + + // Don't use the supplied testClass; instead ensure that we are + // building the MCC for the actual test class that declared the + // configuration for the current level in the context hierarchy. + Assert.notEmpty(reversedList, "ContextConfigurationAttributes list must not be empty"); + Class declaringClass = reversedList.get(0).getDeclaringClass(); + + mergedConfig = buildMergedContextConfiguration(declaringClass, reversedList, parentConfig, + cacheAwareContextLoaderDelegate); + parentConfig = mergedConfig; + } + + // Return the last level in the context hierarchy + return mergedConfig; + } + else { + return buildMergedContextConfiguration(testClass, + ContextLoaderUtils.resolveContextConfigurationAttributes(testClass), null, + cacheAwareContextLoaderDelegate); + } + } + + /** + * Build the {@link MergedContextConfiguration merged context configuration} + * for the supplied {@link Class testClass}, context configuration attributes, + * and parent context configuration. + * + * @param testClass the test class for which the {@code MergedContextConfiguration} + * should be built (must not be {@code null}) + * @param configAttributesList the list of context configuration attributes for the + * specified test class, ordered bottom-up (i.e., as if we were + * traversing up the class hierarchy); never {@code null} or empty + * @param parentConfig the merged context configuration for the parent application + * context in a context hierarchy, or {@code null} if there is no parent + * @param cacheAwareContextLoaderDelegate the cache-aware context loader delegate to + * be passed to the {@code MergedContextConfiguration} constructor + * @return the merged context configuration + * @see #resolveContextLoader + * @see ContextLoaderUtils#resolveContextConfigurationAttributes + * @see SmartContextLoader#processContextConfiguration + * @see ContextLoader#processLocations + * @see ActiveProfilesUtils#resolveActiveProfiles + * @see ApplicationContextInitializerUtils#resolveInitializerClasses + * @see MergedContextConfiguration + */ + private MergedContextConfiguration buildMergedContextConfiguration(final Class testClass, + final List configAttributesList, MergedContextConfiguration parentConfig, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { + + final ContextLoader contextLoader = resolveContextLoader(testClass, configAttributesList); + final List locationsList = new ArrayList(); + final List> classesList = new ArrayList>(); + + for (ContextConfigurationAttributes configAttributes : configAttributesList) { + if (logger.isTraceEnabled()) { + logger.trace(String.format("Processing locations and classes for context configuration attributes %s", + configAttributes)); + } + + if (contextLoader instanceof SmartContextLoader) { + SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader; + smartContextLoader.processContextConfiguration(configAttributes); + locationsList.addAll(0, Arrays.asList(configAttributes.getLocations())); + classesList.addAll(0, Arrays.asList(configAttributes.getClasses())); + } + else { + String[] processedLocations = contextLoader.processLocations(configAttributes.getDeclaringClass(), + configAttributes.getLocations()); + locationsList.addAll(0, Arrays.asList(processedLocations)); + // Legacy ContextLoaders don't know how to process classes + } + + if (!configAttributes.isInheritLocations()) { + break; + } + } + + String[] locations = StringUtils.toStringArray(locationsList); + Class[] classes = ClassUtils.toClassArray(classesList); + Set>> initializerClasses = // + ApplicationContextInitializerUtils.resolveInitializerClasses(configAttributesList); + String[] activeProfiles = ActiveProfilesUtils.resolveActiveProfiles(testClass); + + return buildMergedContextConfiguration(testClass, locations, classes, initializerClasses, activeProfiles, + contextLoader, cacheAwareContextLoaderDelegate, parentConfig); + } + + /** + * Resolve the {@link ContextLoader} {@linkplain Class class} to use for the + * supplied list of {@link ContextConfigurationAttributes} and then instantiate + * and return that {@code ContextLoader}. + * + *

        If the user has not explicitly declared which loader to use, the value + * returned from {@link #getDefaultContextLoaderClass} will be used as the + * default context loader class. For details on the class resolution process, + * see {@link #resolveExplicitContextLoaderClass} and + * {@link #getDefaultContextLoaderClass}. + * + * @param testClass the test class for which the {@code ContextLoader} should be + * resolved; must not be {@code null} + * @param configAttributesList the list of configuration attributes to process; must + * not be {@code null} or empty; must be ordered bottom-up + * (i.e., as if we were traversing up the class hierarchy) + * @return the resolved {@code ContextLoader} for the supplied {@code testClass} + * (never {@code null}) + */ + private ContextLoader resolveContextLoader(Class testClass, + List configAttributesList) { + Assert.notNull(testClass, "Class must not be null"); + Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty"); + + Class contextLoaderClass = resolveExplicitContextLoaderClass(configAttributesList); + if (contextLoaderClass == null) { + contextLoaderClass = getDefaultContextLoaderClass(testClass); + } + + if (logger.isTraceEnabled()) { + logger.trace(String.format("Using ContextLoader class [%s] for test class [%s]", + contextLoaderClass.getName(), testClass.getName())); + } + + return BeanUtils.instantiateClass(contextLoaderClass, ContextLoader.class); + } + + /** + * Resolve the {@link ContextLoader} {@linkplain Class class} to use for the supplied + * list of {@link ContextConfigurationAttributes}. + * + *

        Beginning with the first level in the context configuration attributes hierarchy: + * + *

          + *
        1. If the {@link ContextConfigurationAttributes#getContextLoaderClass() + * contextLoaderClass} property of {@link ContextConfigurationAttributes} is + * configured with an explicit class, that class will be returned.
        2. + *
        3. If an explicit {@code ContextLoader} class is not specified at the current + * level in the hierarchy, traverse to the next level in the hierarchy and return to + * step #1.
        4. + *
        + * + * @param configAttributesList the list of configuration attributes to process; + * must not be {@code null} or empty; must be ordered bottom-up + * (i.e., as if we were traversing up the class hierarchy) + * @return the {@code ContextLoader} class to use for the supplied configuration + * attributes, or {@code null} if no explicit loader is found + * @throws IllegalArgumentException if supplied configuration attributes are + * {@code null} or empty + */ + private Class resolveExplicitContextLoaderClass( + List configAttributesList) { + Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty"); + + for (ContextConfigurationAttributes configAttributes : configAttributesList) { + if (logger.isTraceEnabled()) { + logger.trace(String.format("Resolving ContextLoader for context configuration attributes %s", + configAttributes)); + } + + Class contextLoaderClass = configAttributes.getContextLoaderClass(); + if (!ContextLoader.class.equals(contextLoaderClass)) { + if (logger.isDebugEnabled()) { + logger.debug(String.format( + "Found explicit ContextLoader class [%s] for context configuration attributes %s", + contextLoaderClass.getName(), configAttributes)); + } + return contextLoaderClass; + } + } + + return null; + } + + /** + * Get the names of the default {@link TestExecutionListener} classes for + * this bootstrapper. + *

        This method is invoked by {@link #getDefaultTestExecutionListenerClasses()}. + * @return an unmodifiable list of names of default {@code + * TestExecutionListener} classes + */ + protected abstract List getDefaultTestExecutionListenerClassNames(); + + /** + * Determine the default {@link ContextLoader} class to use for the supplied + * test class. + *

        The class returned by this method will only be used if a {@code ContextLoader} + * class has not been explicitly declared via {@link ContextConfiguration#loader}. + * @param testClass the test class for which to retrieve the default + * {@code ContextLoader} class + */ + protected abstract Class getDefaultContextLoaderClass(Class testClass); + + /** + * Build a {@link MergedContextConfiguration} instance from the supplied, + * merged values. + * + *

        Concrete subclasses typically will only need to instantiate + * {@link MergedContextConfiguration} (or a specialized subclass thereof) + * from the provided values; further processing and merging of values is likely + * unnecessary. + * + * @param testClass the test class for which the {@code MergedContextConfiguration} + * should be built (must not be {@code null}) + * @param locations the merged resource locations + * @param classes the merged annotated classes + * @param initializerClasses the merged context initializer classes + * @param activeProfiles the merged active bean definition profiles + * @param contextLoader the resolved {@code ContextLoader} + * @param cacheAwareContextLoaderDelegate the cache-aware context loader delegate to + * be provided to the instantiated {@code MergedContextConfiguration} + * @param parentConfig the merged context configuration for the parent application + * context in a context hierarchy, or {@code null} if there is no parent + * @return the fully initialized {@code MergedContextConfiguration} + */ + protected abstract MergedContextConfiguration buildMergedContextConfiguration( + Class testClass, + String[] locations, + Class[] classes, + Set>> initializerClasses, + String[] activeProfiles, ContextLoader contextLoader, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parentConfig); + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java new file mode 100644 index 00000000000..22b8cfdb5d8 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2014 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.HashSet; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ActiveProfilesResolver; +import org.springframework.test.util.MetaAnnotationUtils; +import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * Utility methods for working with {@link ActiveProfiles @ActiveProfiles} and + * {@link ActiveProfilesResolver ActiveProfilesResolvers}. + * + *

        Although {@code ActiveProfilesUtils} was first introduced in Spring Framework + * 4.1, the initial implementations of methods in this class were based on the + * existing code base in {@code ContextLoaderUtils}. + * + * @author Sam Brannen + * @author Michail Nikolaev + * @since 4.1 + * @see ActiveProfiles + * @see ActiveProfilesResolver + */ +abstract class ActiveProfilesUtils { + + private static final Log logger = LogFactory.getLog(ActiveProfilesUtils.class); + + + private ActiveProfilesUtils() { + /* no-op */ + } + + /** + * Resolve active bean definition profiles for the supplied {@link Class}. + * + *

        Note that the {@link ActiveProfiles#inheritProfiles inheritProfiles} flag of + * {@link ActiveProfiles @ActiveProfiles} will be taken into consideration. + * Specifically, if the {@code inheritProfiles} flag is set to {@code true}, profiles + * defined in the test class will be merged with those defined in superclasses. + * + * @param testClass the class for which to resolve the active profiles (must not be + * {@code null}) + * @return the set of active profiles for the specified class, including active + * profiles from superclasses if appropriate (never {@code null}) + * @see ActiveProfiles + * @see ActiveProfilesResolver + * @see org.springframework.context.annotation.Profile + */ + static String[] resolveActiveProfiles(Class testClass) { + Assert.notNull(testClass, "Class must not be null"); + + final Set activeProfiles = new HashSet(); + + Class annotationType = ActiveProfiles.class; + AnnotationDescriptor descriptor = MetaAnnotationUtils.findAnnotationDescriptor(testClass, + annotationType); + if (descriptor == null && logger.isDebugEnabled()) { + logger.debug(String.format( + "Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]", + annotationType.getName(), testClass.getName())); + } + + while (descriptor != null) { + Class rootDeclaringClass = descriptor.getRootDeclaringClass(); + Class declaringClass = descriptor.getDeclaringClass(); + + AnnotationAttributes annAttrs = descriptor.getAnnotationAttributes(); + if (logger.isTraceEnabled()) { + logger.trace(String.format("Retrieved @ActiveProfiles attributes [%s] for declaring class [%s].", + annAttrs, declaringClass.getName())); + } + validateActiveProfilesConfiguration(declaringClass, annAttrs); + + Class resolverClass = annAttrs.getClass("resolver"); + if (ActiveProfilesResolver.class.equals(resolverClass)) { + resolverClass = DefaultActiveProfilesResolver.class; + } + + ActiveProfilesResolver resolver = null; + try { + resolver = BeanUtils.instantiateClass(resolverClass, ActiveProfilesResolver.class); + } + catch (Exception e) { + String msg = String.format("Could not instantiate ActiveProfilesResolver of " + + "type [%s] for test class [%s].", resolverClass.getName(), rootDeclaringClass.getName()); + logger.error(msg); + throw new IllegalStateException(msg, e); + } + + String[] profiles = resolver.resolve(rootDeclaringClass); + if (profiles == null) { + String msg = String.format( + "ActiveProfilesResolver [%s] returned a null array of bean definition profiles.", + resolverClass.getName()); + logger.error(msg); + throw new IllegalStateException(msg); + } + + for (String profile : profiles) { + if (StringUtils.hasText(profile)) { + activeProfiles.add(profile.trim()); + } + } + + descriptor = annAttrs.getBoolean("inheritProfiles") ? MetaAnnotationUtils.findAnnotationDescriptor( + rootDeclaringClass.getSuperclass(), annotationType) : null; + } + + return StringUtils.toStringArray(activeProfiles); + } + + private static void validateActiveProfilesConfiguration(Class declaringClass, AnnotationAttributes annAttrs) { + String[] valueProfiles = annAttrs.getStringArray("value"); + String[] profiles = annAttrs.getStringArray("profiles"); + boolean valueDeclared = !ObjectUtils.isEmpty(valueProfiles); + boolean profilesDeclared = !ObjectUtils.isEmpty(profiles); + + if (valueDeclared && profilesDeclared) { + String msg = String.format("Class [%s] has been configured with @ActiveProfiles' 'value' [%s] " + + "and 'profiles' [%s] attributes. Only one declaration of active bean " + + "definition profiles is permitted per @ActiveProfiles annotation.", declaringClass.getName(), + ObjectUtils.nullSafeToString(valueProfiles), ObjectUtils.nullSafeToString(profiles)); + logger.error(msg); + throw new IllegalStateException(msg); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/ApplicationContextInitializerUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/ApplicationContextInitializerUtils.java new file mode 100644 index 00000000000..f9b196b4e8d --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/ApplicationContextInitializerUtils.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2014 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.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.util.Assert; + +/** + * Utility methods for working with {@link ApplicationContextInitializer + * ApplicationContextInitializers}. + * + *

        Although {@code ApplicationContextInitializerUtils} was first introduced + * in Spring Framework 4.1, the initial implementations of methods in this class + * were based on the existing code base in {@code ContextLoaderUtils}. + * + * @author Sam Brannen + * @since 4.1 + * @see ContextConfiguration#initializers + */ +abstract class ApplicationContextInitializerUtils { + + private static final Log logger = LogFactory.getLog(ApplicationContextInitializerUtils.class); + + + private ApplicationContextInitializerUtils() { + /* no-op */ + } + + /** + * Resolve the set of merged {@code ApplicationContextInitializer} classes for the + * supplied list of {@code ContextConfigurationAttributes}. + * + *

        Note that the {@link ContextConfiguration#inheritInitializers inheritInitializers} + * flag of {@link ContextConfiguration @ContextConfiguration} will be taken into + * consideration. Specifically, if the {@code inheritInitializers} flag is set to + * {@code true} for a given level in the class hierarchy represented by the provided + * configuration attributes, context initializer classes defined at the given level + * will be merged with those defined in higher levels of the class hierarchy. + * + * @param configAttributesList the list of configuration attributes to process; must + * not be {@code null} or empty; must be ordered bottom-up + * (i.e., as if we were traversing up the class hierarchy) + * @return the set of merged context initializer classes, including those from + * superclasses if appropriate (never {@code null}) + * @since 3.2 + */ + static Set>> resolveInitializerClasses( + List configAttributesList) { + Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty"); + + final Set>> initializerClasses = // + new HashSet>>(); + + for (ContextConfigurationAttributes configAttributes : configAttributesList) { + if (logger.isTraceEnabled()) { + logger.trace(String.format("Processing context initializers for context configuration attributes %s", + configAttributes)); + } + + initializerClasses.addAll(Arrays.asList(configAttributes.getInitializers())); + + if (!configAttributes.isInheritInitializers()) { + break; + } + } + + return initializerClasses; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java new file mode 100644 index 00000000000..edca68e314c --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java @@ -0,0 +1,311 @@ +/* + * Copyright 2002-2014 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.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.core.annotation.AnnotationAttributes; +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.SmartContextLoader; +import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor; +import org.springframework.test.util.MetaAnnotationUtils.UntypedAnnotationDescriptor; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import static org.springframework.core.annotation.AnnotationUtils.*; +import static org.springframework.test.util.MetaAnnotationUtils.*; + +/** + * Utility methods for working with {@link ContextLoader ContextLoaders} and + * {@link SmartContextLoader SmartContextLoaders} and resolving resource locations, + * annotated classes, and application context initializers. + * + * @author Sam Brannen + * @since 3.1 + * @see ContextLoader + * @see SmartContextLoader + * @see ContextConfiguration + * @see ContextConfigurationAttributes + * @see ApplicationContextInitializer + * @see ContextHierarchy + */ +abstract class ContextLoaderUtils { + + static final String GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX = "ContextHierarchyLevel#"; + + private static final Log logger = LogFactory.getLog(ContextLoaderUtils.class); + + + private ContextLoaderUtils() { + /* no-op */ + } + + /** + * Convenience method for creating a {@link ContextConfigurationAttributes} + * instance from the supplied {@link ContextConfiguration} annotation and + * declaring class and then adding the attributes to the supplied list. + */ + private static void convertContextConfigToConfigAttributesAndAddToList(ContextConfiguration contextConfiguration, + Class declaringClass, final List attributesList) { + if (logger.isTraceEnabled()) { + logger.trace(String.format("Retrieved @ContextConfiguration [%s] for declaring class [%s].", + contextConfiguration, declaringClass.getName())); + } + + ContextConfigurationAttributes attributes = new ContextConfigurationAttributes(declaringClass, + contextConfiguration); + if (logger.isTraceEnabled()) { + logger.trace("Resolved context configuration attributes: " + attributes); + } + attributesList.add(attributes); + } + + /** + * Convenience method for creating a {@link ContextConfigurationAttributes} + * instance from the supplied {@link AnnotationAttributes} and declaring + * class and then adding the attributes to the supplied list. + * @since 4.0 + */ + private static void convertAnnotationAttributesToConfigAttributesAndAddToList(AnnotationAttributes annAttrs, + Class declaringClass, final List attributesList) { + if (logger.isTraceEnabled()) { + logger.trace(String.format("Retrieved @ContextConfiguration attributes [%s] for declaring class [%s].", + annAttrs, declaringClass.getName())); + } + + ContextConfigurationAttributes attributes = new ContextConfigurationAttributes(declaringClass, annAttrs); + if (logger.isTraceEnabled()) { + logger.trace("Resolved context configuration attributes: " + attributes); + } + attributesList.add(attributes); + } + + /** + * Resolve the list of lists of {@linkplain ContextConfigurationAttributes context + * configuration attributes} for the supplied {@linkplain Class test class} and its + * superclasses, taking into account context hierarchies declared via + * {@link ContextHierarchy @ContextHierarchy} and + * {@link ContextConfiguration @ContextConfiguration}. + * + *

        The outer list represents a top-down ordering of context configuration + * attributes, where each element in the list represents the context configuration + * declared on a given test class in the class hierarchy. Each nested list + * contains the context configuration attributes declared either via a single + * instance of {@code @ContextConfiguration} on the particular class or via + * multiple instances of {@code @ContextConfiguration} declared within a + * single {@code @ContextHierarchy} instance on the particular class. + * Furthermore, each nested list maintains the order in which + * {@code @ContextConfiguration} instances are declared. + * + *

        Note that the {@link ContextConfiguration#inheritLocations inheritLocations} and + * {@link ContextConfiguration#inheritInitializers() inheritInitializers} flags of + * {@link ContextConfiguration @ContextConfiguration} will not + * be taken into consideration. If these flags need to be honored, that must be + * handled manually when traversing the nested lists returned by this method. + * + * @param testClass the class for which to resolve the context hierarchy attributes + * (must not be {@code null}) + * @return the list of lists of configuration attributes for the specified class; + * never {@code null} + * @throws IllegalArgumentException if the supplied class is {@code null}; if + * neither {@code @ContextConfiguration} nor {@code @ContextHierarchy} is + * present on the supplied class; or if a test class or composed annotation + * in the class hierarchy declares both {@code @ContextConfiguration} and + * {@code @ContextHierarchy} as top-level annotations. + * @throws IllegalStateException if no class in the class hierarchy declares + * {@code @ContextHierarchy}. + * + * @since 3.2.2 + * @see #buildContextHierarchyMap(Class) + * @see #resolveContextConfigurationAttributes(Class) + */ + @SuppressWarnings("unchecked") + static List> resolveContextHierarchyAttributes(Class testClass) { + Assert.notNull(testClass, "Class must not be null"); + Assert.state(findAnnotation(testClass, ContextHierarchy.class) != null, "@ContextHierarchy must be present"); + + final Class contextConfigType = ContextConfiguration.class; + final Class contextHierarchyType = ContextHierarchy.class; + final List> hierarchyAttributes = new ArrayList>(); + + UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(testClass, contextConfigType, + contextHierarchyType); + Assert.notNull(descriptor, String.format( + "Could not find an 'annotation declaring class' for annotation type [%s] or [%s] and test class [%s]", + contextConfigType.getName(), contextHierarchyType.getName(), testClass.getName())); + + while (descriptor != null) { + Class rootDeclaringClass = descriptor.getRootDeclaringClass(); + Class declaringClass = descriptor.getDeclaringClass(); + + boolean contextConfigDeclaredLocally = isAnnotationDeclaredLocally(contextConfigType, declaringClass); + boolean contextHierarchyDeclaredLocally = isAnnotationDeclaredLocally(contextHierarchyType, declaringClass); + + if (contextConfigDeclaredLocally && contextHierarchyDeclaredLocally) { + String msg = String.format("Class [%s] has been configured with both @ContextConfiguration " + + "and @ContextHierarchy. Only one of these annotations may be declared on a test class " + + "or composed annotation.", declaringClass.getName()); + logger.error(msg); + throw new IllegalStateException(msg); + } + + final List configAttributesList = new ArrayList(); + + if (contextConfigDeclaredLocally) { + convertAnnotationAttributesToConfigAttributesAndAddToList(descriptor.getAnnotationAttributes(), + rootDeclaringClass, configAttributesList); + } + else if (contextHierarchyDeclaredLocally) { + ContextHierarchy contextHierarchy = getAnnotation(declaringClass, contextHierarchyType); + for (ContextConfiguration contextConfiguration : contextHierarchy.value()) { + convertContextConfigToConfigAttributesAndAddToList(contextConfiguration, rootDeclaringClass, + configAttributesList); + } + } + else { + // This should theoretically never happen... + String msg = String.format("Test class [%s] has been configured with neither @ContextConfiguration " + + "nor @ContextHierarchy as a class-level annotation.", rootDeclaringClass.getName()); + logger.error(msg); + throw new IllegalStateException(msg); + } + + hierarchyAttributes.add(0, configAttributesList); + + descriptor = findAnnotationDescriptorForTypes(rootDeclaringClass.getSuperclass(), contextConfigType, + contextHierarchyType); + } + + return hierarchyAttributes; + } + + /** + * Build a context hierarchy map for the supplied {@linkplain Class + * test class} and its superclasses, taking into account context hierarchies + * declared via {@link ContextHierarchy @ContextHierarchy} and + * {@link ContextConfiguration @ContextConfiguration}. + * + *

        Each value in the map represents the consolidated list of {@linkplain + * ContextConfigurationAttributes context configuration attributes} for a + * given level in the context hierarchy (potentially across the test class + * hierarchy), keyed by the {@link ContextConfiguration#name() name} of the + * context hierarchy level. + * + *

        If a given level in the context hierarchy does not have an explicit + * name (i.e., configured via {@link ContextConfiguration#name}), a name will + * be generated for that hierarchy level by appending the numerical level to + * the {@link #GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX}. + * + * @param testClass the class for which to resolve the context hierarchy map + * (must not be {@code null}) + * @return a map of context configuration attributes for the context hierarchy, + * keyed by context hierarchy level name; never {@code null} + * @throws IllegalArgumentException if the lists of context configuration + * attributes for each level in the {@code @ContextHierarchy} do not define + * unique context configuration within the overall hierarchy. + * + * @since 3.2.2 + * @see #resolveContextHierarchyAttributes(Class) + */ + static Map> buildContextHierarchyMap(Class testClass) { + final Map> map = new LinkedHashMap>(); + int hierarchyLevel = 1; + + for (List configAttributesList : resolveContextHierarchyAttributes(testClass)) { + for (ContextConfigurationAttributes configAttributes : configAttributesList) { + String name = configAttributes.getName(); + + // Assign a generated name? + if (!StringUtils.hasText(name)) { + name = GENERATED_CONTEXT_HIERARCHY_LEVEL_PREFIX + hierarchyLevel; + } + + // Encountered a new context hierarchy level? + if (!map.containsKey(name)) { + hierarchyLevel++; + map.put(name, new ArrayList()); + } + + map.get(name).add(configAttributes); + } + } + + // Check for uniqueness + Set> set = new HashSet>(map.values()); + if (set.size() != map.size()) { + String msg = String.format("The @ContextConfiguration elements configured via " + + "@ContextHierarchy in test class [%s] and its superclasses must " + + "define unique contexts per hierarchy level.", testClass.getName()); + logger.error(msg); + throw new IllegalStateException(msg); + } + + return map; + } + + /** + * Resolve the list of {@linkplain ContextConfigurationAttributes context + * configuration attributes} for the supplied {@linkplain Class test class} and its + * superclasses. + * + *

        Note that the {@link ContextConfiguration#inheritLocations inheritLocations} and + * {@link ContextConfiguration#inheritInitializers() inheritInitializers} flags of + * {@link ContextConfiguration @ContextConfiguration} will not + * be taken into consideration. If these flags need to be honored, that must be + * handled manually when traversing the list returned by this method. + * + * @param testClass the class for which to resolve the configuration attributes (must + * not be {@code null}) + * @return the list of configuration attributes for the specified class, ordered + * bottom-up (i.e., as if we were traversing up the class hierarchy); + * never {@code null} + * @throws IllegalArgumentException if the supplied class is {@code null} or if + * {@code @ContextConfiguration} is not present on the supplied class + */ + static List resolveContextConfigurationAttributes(Class testClass) { + Assert.notNull(testClass, "Class must not be null"); + + final List attributesList = new ArrayList(); + + Class annotationType = ContextConfiguration.class; + + AnnotationDescriptor descriptor = findAnnotationDescriptor(testClass, annotationType); + Assert.notNull(descriptor, String.format( + "Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]", + annotationType.getName(), testClass.getName())); + + while (descriptor != null) { + convertAnnotationAttributesToConfigAttributesAndAddToList(descriptor.getAnnotationAttributes(), + descriptor.getRootDeclaringClass(), attributesList); + descriptor = findAnnotationDescriptor(descriptor.getRootDeclaringClass().getSuperclass(), annotationType); + } + + return attributesList; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DefaultActiveProfilesResolver.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultActiveProfilesResolver.java new file mode 100644 index 00000000000..46b1cb46a4f --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultActiveProfilesResolver.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2014 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.HashSet; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ActiveProfilesResolver; +import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import static org.springframework.test.util.MetaAnnotationUtils.*; + +/** + * Default implementation of the {@link ActiveProfilesResolver} strategy that + * resolves active bean definition profiles based solely on profiles + * configured declaratively via {@link ActiveProfiles#profiles} or + * {@link ActiveProfiles#value}. + * + * @author Sam Brannen + * @since 4.1 + * @see ActiveProfiles + * @see ActiveProfilesResolver + */ +public class DefaultActiveProfilesResolver implements ActiveProfilesResolver { + + private static final Log logger = LogFactory.getLog(DefaultActiveProfilesResolver.class); + + + /** + * Resolve the bean definition profiles for the given {@linkplain + * Class test class} based on profiles configured declaratively via + * {@link ActiveProfiles#profiles} or {@link ActiveProfiles#value}. + * @param testClass the test class for which the profiles should be resolved; + * never {@code null} + * @return the list of bean definition profiles to use when loading the + * {@code ApplicationContext}; never {@code null} + */ + @Override + public String[] resolve(Class testClass) { + Assert.notNull(testClass, "Class must not be null"); + + final Set activeProfiles = new HashSet(); + + Class annotationType = ActiveProfiles.class; + AnnotationDescriptor descriptor = findAnnotationDescriptor(testClass, annotationType); + + if (descriptor == null) { + if (logger.isDebugEnabled()) { + logger.debug(String.format( + "Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]", + annotationType.getName(), testClass.getName())); + } + } + else { + Class rootDeclaringClass = descriptor.getRootDeclaringClass(); + Class declaringClass = descriptor.getDeclaringClass(); + + AnnotationAttributes annAttrs = descriptor.getAnnotationAttributes(); + if (logger.isTraceEnabled()) { + logger.trace(String.format("Retrieved @ActiveProfiles attributes [%s] for declaring class [%s].", + annAttrs, declaringClass.getName())); + } + + Class resolverClass = annAttrs.getClass("resolver"); + if (!ActiveProfilesResolver.class.equals(resolverClass)) { + String msg = String.format("Configuration error for test class [%s]: %s cannot be used " + + "in conjunction with custom resolver [%s].", rootDeclaringClass.getName(), + getClass().getSimpleName(), resolverClass.getName()); + logger.error(msg); + throw new IllegalStateException(msg); + } + + String[] profiles = annAttrs.getStringArray("profiles"); + String[] valueProfiles = annAttrs.getStringArray("value"); + boolean valueDeclared = !ObjectUtils.isEmpty(valueProfiles); + boolean profilesDeclared = !ObjectUtils.isEmpty(profiles); + + if (valueDeclared && profilesDeclared) { + String msg = String.format("Class [%s] has been configured with @ActiveProfiles' 'value' [%s] " + + "and 'profiles' [%s] attributes. Only one declaration of active bean " + + "definition profiles is permitted per @ActiveProfiles annotation.", declaringClass.getName(), + ObjectUtils.nullSafeToString(valueProfiles), ObjectUtils.nullSafeToString(profiles)); + logger.error(msg); + throw new IllegalStateException(msg); + } + + if (valueDeclared) { + profiles = valueProfiles; + } + + for (String profile : profiles) { + if (StringUtils.hasText(profile)) { + activeProfiles.add(profile.trim()); + } + } + } + + return StringUtils.toStringArray(activeProfiles); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContextBootstrapper.java new file mode 100644 index 00000000000..d3cf3f87762 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContextBootstrapper.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2014 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.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.CacheAwareContextLoaderDelegate; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContextBootstrapper; +import org.springframework.test.context.TestExecutionListener; + +/** + * Default implementation of the {@link TestContextBootstrapper} SPI. + * + *

          + *
        • Uses the following default {@link TestExecutionListener TestExecutionListeners}: + *
            + *
          1. {@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener} + *
          2. {@link org.springframework.test.context.support.DirtiesContextTestExecutionListener} + *
          3. {@link org.springframework.test.context.transaction.TransactionalTestExecutionListener} + *
          + *
        • Uses {@link DelegatingSmartContextLoader} as the default {@link ContextLoader}. + *
        • Builds a standard {@link MergedContextConfiguration}. + *
        + * + * @author Sam Brannen + * @since 4.1 + */ +public class DefaultTestContextBootstrapper extends AbstractTestContextBootstrapper { + + private static final List DEFAULT_TEST_EXECUTION_LISTENER_CLASS_NAMES = Collections.unmodifiableList(Arrays.asList( + "org.springframework.test.context.support.DependencyInjectionTestExecutionListener", + "org.springframework.test.context.support.DirtiesContextTestExecutionListener", + "org.springframework.test.context.transaction.TransactionalTestExecutionListener")); + + + /** + * Returns an unmodifiable list of fully qualified class names for the following + * default {@link TestExecutionListener TestExecutionListeners}: + *
          + *
        1. {@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener} + *
        2. {@link org.springframework.test.context.support.DirtiesContextTestExecutionListener} + *
        3. {@link org.springframework.test.context.transaction.TransactionalTestExecutionListener} + *
        + */ + protected List getDefaultTestExecutionListenerClassNames() { + return DEFAULT_TEST_EXECUTION_LISTENER_CLASS_NAMES; + } + + /** + * Returns {@link DelegatingSmartContextLoader}. + */ + @Override + protected Class getDefaultContextLoaderClass(Class testClass) { + return DelegatingSmartContextLoader.class; + } + + /** + * Builds a standard {@link MergedContextConfiguration}. + */ + protected MergedContextConfiguration buildMergedContextConfiguration( + Class testClass, + String[] locations, + Class[] classes, + Set>> initializerClasses, + String[] activeProfiles, ContextLoader contextLoader, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parentConfig) { + + return new MergedContextConfiguration(testClass, locations, classes, initializerClasses, activeProfiles, + contextLoader, cacheAwareContextLoaderDelegate, parentConfig); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java index 07746c1e142..b2434c5879e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2014 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,6 +23,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.test.context.BootstrapWith; + /** * {@code @WebAppConfiguration} is a class-level annotation that is used to * declare that the {@code ApplicationContext} loaded for an integration test @@ -51,6 +53,7 @@ import java.lang.annotation.Target; @Inherited @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) +@BootstrapWith(WebTestContextBootstrapper.class) public @interface WebAppConfiguration { /** diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java new file mode 100644 index 00000000000..46b0e4ad292 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2014 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.web; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.test.context.CacheAwareContextLoaderDelegate; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContextBootstrapper; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.support.DefaultTestContextBootstrapper; + +/** + * Web-specific implementation of the {@link TestContextBootstrapper} SPI. + * + *
          + *
        • Prepends {@link org.springframework.test.context.web.ServletTestExecutionListener} + * to the list of default {@link TestExecutionListener TestExecutionListeners} supported by + * the superclass. + *
        • Uses {@link WebDelegatingSmartContextLoader} as the default {@link ContextLoader} + * if the test class is annotated with {@link WebAppConfiguration @WebAppConfiguration} + * and otherwise delegates to the superclass. + *
        • Builds a {@link WebMergedContextConfiguration} if the test class is annotated + * with {@link WebAppConfiguration @WebAppConfiguration} and otherwise delegates to + * the superclass. + *
        + * + * @author Sam Brannen + * @since 4.1 + */ +public class WebTestContextBootstrapper extends DefaultTestContextBootstrapper { + + /** + * Prepends {@link org.springframework.test.context.web.ServletTestExecutionListener} + * to the list of default {@link TestExecutionListener TestExecutionListeners} + * supported by the superclass and returns an unmodifiable, updated list. + */ + @Override + protected List getDefaultTestExecutionListenerClassNames() { + List classNames = new ArrayList(super.getDefaultTestExecutionListenerClassNames()); + classNames.add(0, "org.springframework.test.context.web.ServletTestExecutionListener"); + return Collections.unmodifiableList(classNames); + } + + /** + * Returns {@link WebDelegatingSmartContextLoader} if the supplied class is + * annotated with {@link WebAppConfiguration @WebAppConfiguration} and + * otherwise delegates to the superclass. + */ + @Override + protected Class getDefaultContextLoaderClass(Class testClass) { + if (AnnotationUtils.findAnnotation(testClass, WebAppConfiguration.class) != null) { + return WebDelegatingSmartContextLoader.class; + } + + // else... + return super.getDefaultContextLoaderClass(testClass); + } + + /** + * Builds a {@link WebMergedContextConfiguration} if the supplied class is + * annotated with {@link WebAppConfiguration @WebAppConfiguration} and + * otherwise delegates to the superclass. + */ + @Override + protected MergedContextConfiguration buildMergedContextConfiguration( + Class testClass, + String[] locations, + Class[] classes, + Set>> initializerClasses, + String[] activeProfiles, ContextLoader contextLoader, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parentConfig) { + + WebAppConfiguration webAppConfiguration = AnnotationUtils.findAnnotation(testClass, WebAppConfiguration.class); + if (webAppConfiguration != null) { + String resourceBasePath = webAppConfiguration.value(); + + return new WebMergedContextConfiguration(testClass, locations, classes, initializerClasses, activeProfiles, + resourceBasePath, contextLoader, cacheAwareContextLoaderDelegate, parentConfig); + } + + // else... + return super.buildMergedContextConfiguration(testClass, locations, classes, initializerClasses, activeProfiles, + contextLoader, cacheAwareContextLoaderDelegate, parentConfig); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/MetaAnnotationUtils.java b/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java similarity index 92% rename from spring-test/src/main/java/org/springframework/test/context/MetaAnnotationUtils.java rename to spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java index d926568d91b..6f556909811 100644 --- a/spring-test/src/main/java/org/springframework/test/context/MetaAnnotationUtils.java +++ b/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.util; import java.lang.annotation.Annotation; import java.util.HashSet; @@ -27,8 +27,6 @@ import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; -import static org.springframework.core.annotation.AnnotationUtils.*; - /** * {@code MetaAnnotationUtils} is a collection of utility methods that complements * the standard support already available in {@link AnnotationUtils}. @@ -36,24 +34,26 @@ import static org.springframework.core.annotation.AnnotationUtils.*; *

        Whereas {@code AnnotationUtils} provides utilities for getting or * finding an annotation, {@code MetaAnnotationUtils} goes a step further * by providing support for determining the root class on which an - * annotation is declared, either directly or via a composed annotation. - * This additional information is encapsulated in an {@link AnnotationDescriptor}. + * annotation is declared, either directly or indirectly via a composed + * annotation. This additional information is encapsulated in an + * {@link AnnotationDescriptor}. * *

        The additional information provided by an {@code AnnotationDescriptor} is * required by the Spring TestContext Framework in order to be able to * support class hierarchy traversals for annotations such as - * {@link ContextConfiguration @ContextConfiguration}, - * {@link TestExecutionListeners @TestExecutionListeners}, and - * {@link ActiveProfiles @ActiveProfiles} which offer support for merging and - * overriding various inherited annotation attributes (e.g., {@link - * ContextConfiguration#inheritLocations}). + * {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration}, + * {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners}, + * and {@link org.springframework.test.context.ActiveProfiles @ActiveProfiles} + * which offer support for merging and overriding various inherited + * annotation attributes (e.g., {@link + * org.springframework.test.context.ContextConfiguration#inheritLocations}). * * @author Sam Brannen * @since 4.0 * @see AnnotationUtils * @see AnnotationDescriptor */ -abstract class MetaAnnotationUtils { +public abstract class MetaAnnotationUtils { private MetaAnnotationUtils() { /* no-op */ @@ -117,13 +117,13 @@ abstract class MetaAnnotationUtils { } // Declared locally? - if (isAnnotationDeclaredLocally(annotationType, clazz)) { + if (AnnotationUtils.isAnnotationDeclaredLocally(annotationType, clazz)) { return new AnnotationDescriptor(clazz, clazz.getAnnotation(annotationType)); } // Declared on a composed annotation (i.e., as a meta-annotation)? for (Annotation composedAnnotation : clazz.getDeclaredAnnotations()) { - if (!isInJavaLangAnnotationPackage(composedAnnotation) && visited.add(composedAnnotation)) { + if (!AnnotationUtils.isInJavaLangAnnotationPackage(composedAnnotation) && visited.add(composedAnnotation)) { AnnotationDescriptor descriptor = findAnnotationDescriptor(composedAnnotation.annotationType(), visited, annotationType); if (descriptor != null) { @@ -203,14 +203,14 @@ abstract class MetaAnnotationUtils { // Declared locally? for (Class annotationType : annotationTypes) { - if (isAnnotationDeclaredLocally(annotationType, clazz)) { + if (AnnotationUtils.isAnnotationDeclaredLocally(annotationType, clazz)) { return new UntypedAnnotationDescriptor(clazz, clazz.getAnnotation(annotationType)); } } // Declared on a composed annotation (i.e., as a meta-annotation)? for (Annotation composedAnnotation : clazz.getDeclaredAnnotations()) { - if (!isInJavaLangAnnotationPackage(composedAnnotation) && visited.add(composedAnnotation)) { + if (!AnnotationUtils.isInJavaLangAnnotationPackage(composedAnnotation) && visited.add(composedAnnotation)) { UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes( composedAnnotation.annotationType(), visited, annotationTypes); if (descriptor != null) { 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 new file mode 100644 index 00000000000..7af0039f022 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/BootstrapTestUtils.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2014 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; + +/** + * Collection of test-related utility methods for working with {@link BootstrapContext + * BootstrapContexts} and {@link TestContextBootstrapper TestContextBootstrappers}. + * + * @author Sam Brannen + * @since 4.1 + */ +public abstract class BootstrapTestUtils { + + private BootstrapTestUtils() { + /* no-op */ + } + + public static BootstrapContext buildBootstrapContext(Class testClass, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { + return new DefaultBootstrapContext(testClass, cacheAwareContextLoaderDelegate); + } + + public static TestContextBootstrapper resolveTestContextBootstrapper(BootstrapContext bootstrapContext) { + return BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext); + } + +} 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 d593ddb8e11..8818df46272 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -56,7 +56,7 @@ public class ContextCacheTests { } private ApplicationContext loadContext(Class testClass) { - TestContext testContext = new DefaultTestContext(testClass, contextCache); + TestContext testContext = TestContextTestUtils.buildTestContext(testClass, contextCache); return testContext.getApplicationContext(); } @@ -119,15 +119,15 @@ public class ContextCacheTests { public void removeContextHierarchyCacheLevel1() { // Load Level 3-A - TestContext testContext3a = new DefaultTestContext(ClassHierarchyContextHierarchyLevel3aTestCase.class, - contextCache); + TestContext testContext3a = TestContextTestUtils.buildTestContext( + ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache); testContext3a.getApplicationContext(); assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3); assertParentContextCount(2); // Load Level 3-B - TestContext testContext3b = new DefaultTestContext(ClassHierarchyContextHierarchyLevel3bTestCase.class, - contextCache); + TestContext testContext3b = TestContextTestUtils.buildTestContext( + ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache); testContext3b.getApplicationContext(); assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4); assertParentContextCount(2); @@ -144,15 +144,15 @@ public class ContextCacheTests { public void removeContextHierarchyCacheLevel1WithExhaustiveMode() { // Load Level 3-A - TestContext testContext3a = new DefaultTestContext(ClassHierarchyContextHierarchyLevel3aTestCase.class, - contextCache); + TestContext testContext3a = TestContextTestUtils.buildTestContext( + ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache); testContext3a.getApplicationContext(); assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3); assertParentContextCount(2); // Load Level 3-B - TestContext testContext3b = new DefaultTestContext(ClassHierarchyContextHierarchyLevel3bTestCase.class, - contextCache); + TestContext testContext3b = TestContextTestUtils.buildTestContext( + ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache); testContext3b.getApplicationContext(); assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4); assertParentContextCount(2); @@ -169,15 +169,15 @@ public class ContextCacheTests { public void removeContextHierarchyCacheLevel2() { // Load Level 3-A - TestContext testContext3a = new DefaultTestContext(ClassHierarchyContextHierarchyLevel3aTestCase.class, - contextCache); + TestContext testContext3a = TestContextTestUtils.buildTestContext( + ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache); testContext3a.getApplicationContext(); assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3); assertParentContextCount(2); // Load Level 3-B - TestContext testContext3b = new DefaultTestContext(ClassHierarchyContextHierarchyLevel3bTestCase.class, - contextCache); + TestContext testContext3b = TestContextTestUtils.buildTestContext( + ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache); testContext3b.getApplicationContext(); assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4); assertParentContextCount(2); @@ -195,15 +195,15 @@ public class ContextCacheTests { public void removeContextHierarchyCacheLevel2WithExhaustiveMode() { // Load Level 3-A - TestContext testContext3a = new DefaultTestContext(ClassHierarchyContextHierarchyLevel3aTestCase.class, - contextCache); + TestContext testContext3a = TestContextTestUtils.buildTestContext( + ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache); testContext3a.getApplicationContext(); assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3); assertParentContextCount(2); // Load Level 3-B - TestContext testContext3b = new DefaultTestContext(ClassHierarchyContextHierarchyLevel3bTestCase.class, - contextCache); + TestContext testContext3b = TestContextTestUtils.buildTestContext( + ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache); testContext3b.getApplicationContext(); assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4); assertParentContextCount(2); @@ -219,15 +219,15 @@ public class ContextCacheTests { public void removeContextHierarchyCacheLevel3Then2() { // Load Level 3-A - TestContext testContext3a = new DefaultTestContext(ClassHierarchyContextHierarchyLevel3aTestCase.class, - contextCache); + TestContext testContext3a = TestContextTestUtils.buildTestContext( + ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache); testContext3a.getApplicationContext(); assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3); assertParentContextCount(2); // Load Level 3-B - TestContext testContext3b = new DefaultTestContext(ClassHierarchyContextHierarchyLevel3bTestCase.class, - contextCache); + TestContext testContext3b = TestContextTestUtils.buildTestContext( + ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache); testContext3b.getApplicationContext(); assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4); assertParentContextCount(2); @@ -248,15 +248,15 @@ public class ContextCacheTests { public void removeContextHierarchyCacheLevel3Then2WithExhaustiveMode() { // Load Level 3-A - TestContext testContext3a = new DefaultTestContext(ClassHierarchyContextHierarchyLevel3aTestCase.class, - contextCache); + TestContext testContext3a = TestContextTestUtils.buildTestContext( + ClassHierarchyContextHierarchyLevel3aTestCase.class, contextCache); testContext3a.getApplicationContext(); assertContextCacheStatistics(contextCache, "level 3, A", 3, 0, 3); assertParentContextCount(2); // Load Level 3-B - TestContext testContext3b = new DefaultTestContext(ClassHierarchyContextHierarchyLevel3bTestCase.class, - contextCache); + TestContext testContext3b = TestContextTestUtils.buildTestContext( + ClassHierarchyContextHierarchyLevel3bTestCase.class, contextCache); testContext3b.getApplicationContext(); assertContextCacheStatistics(contextCache, "level 3, A and B", 4, 1, 4); assertParentContextCount(2); 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 new file mode 100644 index 00000000000..94d7dbaba9a --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/TestContextTestUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2014 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; + +/** + * Collection of test-related utility methods for working with {@link TestContext TestContexts}. + * + * @author Sam Brannen + * @since 4.1 + */ +public abstract class TestContextTestUtils { + + private TestContextTestUtils() { + /* no-op */ + } + + public static TestContext buildTestContext(Class testClass, ContextCache contextCache) { + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = new DefaultCacheAwareContextLoaderDelegate( + contextCache); + return buildTestContext(testClass, null, cacheAwareContextLoaderDelegate); + } + + public static TestContext buildTestContext(Class testClass, String customDefaultContextLoaderClassName, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { + BootstrapContext bootstrapContext = new DefaultBootstrapContext(testClass, cacheAwareContextLoaderDelegate); + TestContextBootstrapper testContextBootstrapper = BootstrapUtils.resolveTestContextBootstrapper(bootstrapContext); + + return new DefaultTestContext(testContextBootstrapper); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java index 32483a078cd..5142ee5a3f1 100644 --- a/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -45,7 +45,7 @@ public class TestExecutionListenersTests { @Test public void verifyNumDefaultListenersRegistered() throws Exception { TestContextManager testContextManager = new TestContextManager(DefaultListenersExampleTestCase.class); - assertEquals("Num registered TELs for DefaultListenersExampleTestCase.", 4, + assertEquals("Num registered TELs for DefaultListenersExampleTestCase.", 3, testContextManager.getTestExecutionListeners().size()); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit4/CustomDefaultContextLoaderClassSpringRunnerTests.java b/spring-test/src/test/java/org/springframework/test/context/junit4/CustomDefaultContextLoaderClassSpringRunnerTests.java index 3ee77974a0e..74d5e465d9e 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit4/CustomDefaultContextLoaderClassSpringRunnerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit4/CustomDefaultContextLoaderClassSpringRunnerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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,27 +16,29 @@ package org.springframework.test.context.junit4; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.model.InitializationError; -import org.springframework.tests.sample.beans.Pet; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.BootstrapWith; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.support.DefaultTestContextBootstrapper; import org.springframework.test.context.support.GenericPropertiesContextLoader; +import org.springframework.tests.sample.beans.Pet; + +import static org.junit.Assert.*; /** - * Integration tests which verify that a subclass of {@link SpringJUnit4ClassRunner} - * can specify a custom default ContextLoader class name that overrides - * the standard default class name. + * Integration tests which verify that a subclass of {@link DefaultTestContextBootstrapper} + * can specify a custom default ContextLoader class that overrides the standard + * default class name. * * @author Sam Brannen * @since 3.0 */ -@RunWith(CustomDefaultContextLoaderClassSpringRunnerTests.PropertiesBasedSpringJUnit4ClassRunner.class) -@ContextConfiguration(locations = "PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests-context.properties") +@RunWith(SpringJUnit4ClassRunner.class) +@BootstrapWith(CustomDefaultContextLoaderClassSpringRunnerTests.PropertiesBasedTestContextBootstrapper.class) +@ContextConfiguration("PropertiesBasedSpringJUnit4ClassRunnerAppCtxTests-context.properties") public class CustomDefaultContextLoaderClassSpringRunnerTests { @Autowired @@ -56,16 +58,12 @@ public class CustomDefaultContextLoaderClassSpringRunnerTests { } - public static final class PropertiesBasedSpringJUnit4ClassRunner extends SpringJUnit4ClassRunner { - - public PropertiesBasedSpringJUnit4ClassRunner(Class clazz) throws InitializationError { - super(clazz); - } + public static class PropertiesBasedTestContextBootstrapper extends DefaultTestContextBootstrapper { @Override - protected String getDefaultContextLoaderClassName(Class clazz) { - return GenericPropertiesContextLoader.class.getName(); + protected Class getDefaultContextLoaderClass(Class testClass) { + return GenericPropertiesContextLoader.class; } - } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/AbstractContextLoaderUtilsTests$BareAnnotations-context.xml b/spring-test/src/test/java/org/springframework/test/context/support/AbstractContextLoaderUtilsTests$BareAnnotations-context.xml similarity index 100% rename from spring-test/src/test/java/org/springframework/test/context/AbstractContextLoaderUtilsTests$BareAnnotations-context.xml rename to spring-test/src/test/java/org/springframework/test/context/support/AbstractContextLoaderUtilsTests$BareAnnotations-context.xml diff --git a/spring-test/src/test/java/org/springframework/test/context/AbstractContextLoaderUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/support/AbstractContextLoaderUtilsTests.java similarity index 78% rename from spring-test/src/test/java/org/springframework/test/context/AbstractContextLoaderUtilsTests.java rename to spring-test/src/test/java/org/springframework/test/context/support/AbstractContextLoaderUtilsTests.java index 06515d42695..eae38bc594c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/AbstractContextLoaderUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/AbstractContextLoaderUtilsTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.context.support; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -23,10 +23,19 @@ import java.lang.annotation.Target; import java.util.Collections; import java.util.Set; +import org.mockito.Mockito; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.support.AnnotationConfigContextLoader; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.BootstrapContext; +import org.springframework.test.context.BootstrapTestUtils; +import org.springframework.test.context.CacheAwareContextLoaderDelegate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContextBootstrapper; import static org.junit.Assert.*; @@ -44,6 +53,14 @@ abstract class AbstractContextLoaderUtilsTests { Collections.>> emptySet(); + MergedContextConfiguration buildMergedContextConfiguration(Class testClass) { + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = Mockito.mock(CacheAwareContextLoaderDelegate.class); + BootstrapContext bootstrapContext = BootstrapTestUtils.buildBootstrapContext(testClass, + cacheAwareContextLoaderDelegate); + TestContextBootstrapper bootstrapper = BootstrapTestUtils.resolveTestContextBootstrapper(bootstrapContext); + return bootstrapper.buildMergedContextConfiguration(); + } + void assertAttributes(ContextConfigurationAttributes attributes, Class expectedDeclaringClass, String[] expectedLocations, Class[] expectedClasses, Class expectedContextLoaderClass, boolean expectedInheritLocations) { @@ -173,4 +190,16 @@ abstract class AbstractContextLoaderUtilsTests { static class OverriddenClassesBar extends ClassesFoo { } + @ContextConfiguration(locations = "/foo.properties", loader = GenericPropertiesContextLoader.class) + @ActiveProfiles(profiles = "foo") + static class PropertiesLocationsFoo { + } + + // Combining @Configuration classes with a Properties based loader doesn't really make + // sense, but that's OK for unit testing purposes. + @ContextConfiguration(classes = FooConfig.class, loader = GenericPropertiesContextLoader.class) + @ActiveProfiles(profiles = "foo") + static class PropertiesClassesFoo { + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsActiveProfilesTests.java b/spring-test/src/test/java/org/springframework/test/context/support/ActiveProfilesUtilsTests.java similarity index 90% rename from spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsActiveProfilesTests.java rename to spring-test/src/test/java/org/springframework/test/context/support/ActiveProfilesUtilsTests.java index 76e4e7b7bb0..f9f98b7c73c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsActiveProfilesTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/ActiveProfilesUtilsTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.context.support; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -25,19 +25,21 @@ import java.util.HashSet; import java.util.Set; import org.junit.Test; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ActiveProfilesResolver; import static org.junit.Assert.*; -import static org.springframework.test.context.ContextLoaderUtils.*; +import static org.springframework.test.context.support.ActiveProfilesUtils.*; /** - * Unit tests for {@link ContextLoaderUtils} involving resolution of active bean + * Unit tests for {@link ActiveProfilesUtils} involving resolution of active bean * definition profiles. * * @author Sam Brannen * @author Michail Nikolaev * @since 3.1 */ -public class ContextLoaderUtilsActiveProfilesTests extends AbstractContextLoaderUtilsTests { +public class ActiveProfilesUtilsTests extends AbstractContextLoaderUtilsTests { private void assertResolvedProfiles(Class testClass, String... expected) { assertNotNull(testClass); @@ -168,17 +170,17 @@ public class ContextLoaderUtilsActiveProfilesTests extends AbstractContextLoader /** * @since 4.0 */ - @Test(expected = IllegalStateException.class) - public void resolveActiveProfilesWithConflictingResolverAndProfiles() { - resolveActiveProfiles(ConflictingResolverAndProfilesTestCase.class); + @Test + public void resolveActiveProfilesWithResolverAndProfiles() { + assertResolvedProfiles(ResolverAndProfilesTestCase.class, "bar"); } /** * @since 4.0 */ - @Test(expected = IllegalStateException.class) - public void resolveActiveProfilesWithConflictingResolverAndValue() { - resolveActiveProfiles(ConflictingResolverAndValueTestCase.class); + @Test + public void resolveActiveProfilesWithResolverAndValue() { + assertResolvedProfiles(ResolverAndValueTestCase.class, "bar"); } /** @@ -279,19 +281,19 @@ public class ContextLoaderUtilsActiveProfilesTests extends AbstractContextLoader InheritedFooActiveProfilesResolverTestCase { } - @ActiveProfiles(resolver = BarActiveProfilesResolver.class, profiles = "conflict") - private static class ConflictingResolverAndProfilesTestCase { + @ActiveProfiles(resolver = BarActiveProfilesResolver.class, profiles = "ignored by custom resolver") + private static class ResolverAndProfilesTestCase { } - @ActiveProfiles(resolver = BarActiveProfilesResolver.class, value = "conflict") - private static class ConflictingResolverAndValueTestCase { + @ActiveProfiles(resolver = BarActiveProfilesResolver.class, value = "ignored by custom resolver") + private static class ResolverAndValueTestCase { } @MetaResolverConfig private static class TestClassVerifyingActiveProfilesResolverTestCase { } - @ActiveProfiles(profiles = "conflict", value = "conflict") + @ActiveProfiles(profiles = "conflict 1", value = "conflict 2") private static class ConflictingProfilesAndValueTestCase { } diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsConfigurationAttributesTests.java b/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsConfigurationAttributesTests.java similarity index 94% rename from spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsConfigurationAttributesTests.java rename to spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsConfigurationAttributesTests.java index 6025c2d47fd..a7f9a08ffa3 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsConfigurationAttributesTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsConfigurationAttributesTests.java @@ -14,15 +14,19 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.context.support; import java.util.List; import org.junit.Test; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextLoader; import org.springframework.test.context.support.AnnotationConfigContextLoader; +import org.springframework.test.context.support.ContextLoaderUtils; +import static org.springframework.test.context.support.ContextLoaderUtils.*; import static org.junit.Assert.*; -import static org.springframework.test.context.ContextLoaderUtils.*; /** * Unit tests for {@link ContextLoaderUtils} involving {@link ContextConfigurationAttributes}. diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsContextHierarchyTests.java b/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsContextHierarchyTests.java similarity index 98% rename from spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsContextHierarchyTests.java rename to spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsContextHierarchyTests.java index b1d07c02d14..052128f3931 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsContextHierarchyTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsContextHierarchyTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.context.support; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -25,11 +25,16 @@ import java.util.Map; import org.junit.Test; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; +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.support.AnnotationConfigContextLoader; +import org.springframework.test.context.support.ContextLoaderUtils; +import static org.springframework.test.context.support.ContextLoaderUtils.*; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; -import static org.springframework.test.context.ContextLoaderUtils.*; /** * Unit tests for {@link ContextLoaderUtils} involving context hierarchies. diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsContextInitializerTests.java b/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsContextInitializerTests.java similarity index 93% rename from spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsContextInitializerTests.java rename to spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsContextInitializerTests.java index 654efb2bd6e..03be7bd2567 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsContextInitializerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsContextInitializerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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.util.HashSet; import java.util.Set; @@ -23,11 +23,10 @@ import org.junit.Test; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericApplicationContext; -import org.springframework.test.context.support.DelegatingSmartContextLoader; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.MergedContextConfiguration; import org.springframework.web.context.support.GenericWebApplicationContext; -import static org.springframework.test.context.ContextLoaderUtils.*; - /** * Unit tests for {@link ContextLoaderUtils} involving {@link ApplicationContextInitializer}s. * @@ -44,7 +43,8 @@ public class ContextLoaderUtilsContextInitializerTests extends AbstractContextLo = new HashSet>>(); expectedInitializerClasses.add(FooInitializer.class); - MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, expectedInitializerClasses, DelegatingSmartContextLoader.class); } @@ -58,7 +58,8 @@ public class ContextLoaderUtilsContextInitializerTests extends AbstractContextLo expectedInitializerClasses.add(FooInitializer.class); expectedInitializerClasses.add(BarInitializer.class); - MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, expectedInitializerClasses, DelegatingSmartContextLoader.class); } @@ -71,7 +72,8 @@ public class ContextLoaderUtilsContextInitializerTests extends AbstractContextLo = new HashSet>>(); expectedInitializerClasses.add(BarInitializer.class); - MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, expectedInitializerClasses, DelegatingSmartContextLoader.class); } @@ -84,7 +86,8 @@ public class ContextLoaderUtilsContextInitializerTests extends AbstractContextLo = new HashSet>>(); expectedInitializerClasses.add(BarInitializer.class); - MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); + assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, expectedInitializerClasses, DelegatingSmartContextLoader.class); } diff --git a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsMergedConfigTests.java b/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsMergedConfigTests.java similarity index 79% rename from spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsMergedConfigTests.java rename to spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsMergedConfigTests.java index f9988392a4c..2d84d022f2c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/ContextLoaderUtilsMergedConfigTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/support/ContextLoaderUtilsMergedConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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,14 +14,11 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.context.support; import org.junit.Test; -import org.springframework.test.context.support.AnnotationConfigContextLoader; -import org.springframework.test.context.support.DelegatingSmartContextLoader; -import org.springframework.test.context.support.GenericPropertiesContextLoader; - -import static org.springframework.test.context.ContextLoaderUtils.*; +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.MergedContextConfiguration; /** * Unit tests for {@link ContextLoaderUtils} involving {@link MergedContextConfiguration}. @@ -33,28 +30,28 @@ public class ContextLoaderUtilsMergedConfigTests extends AbstractContextLoaderUt @Test public void buildMergedConfigWithoutAnnotation() { - Class testClass = Enigma.class; - MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + Class testClass = Enigma.class; + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, null); } @Test public void buildMergedConfigWithBareAnnotations() { - Class testClass = BareAnnotations.class; - MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + Class testClass = BareAnnotations.class; + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); assertMergedConfig( mergedConfig, testClass, - new String[] { "classpath:/org/springframework/test/context/AbstractContextLoaderUtilsTests$BareAnnotations-context.xml" }, + new String[] { "classpath:/org/springframework/test/context/support/AbstractContextLoaderUtilsTests$BareAnnotations-context.xml" }, EMPTY_CLASS_ARRAY, DelegatingSmartContextLoader.class); } @Test public void buildMergedConfigWithLocalAnnotationAndLocations() { Class testClass = LocationsFoo.class; - MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); assertMergedConfig(mergedConfig, testClass, new String[] { "classpath:/foo.xml" }, EMPTY_CLASS_ARRAY, DelegatingSmartContextLoader.class); @@ -63,7 +60,7 @@ public class ContextLoaderUtilsMergedConfigTests extends AbstractContextLoaderUt @Test public void buildMergedConfigWithMetaAnnotationAndLocations() { Class testClass = MetaLocationsFoo.class; - MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); assertMergedConfig(mergedConfig, testClass, new String[] { "classpath:/foo.xml" }, EMPTY_CLASS_ARRAY, DelegatingSmartContextLoader.class); @@ -72,7 +69,7 @@ public class ContextLoaderUtilsMergedConfigTests extends AbstractContextLoaderUt @Test public void buildMergedConfigWithLocalAnnotationAndClasses() { Class testClass = ClassesFoo.class; - MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, new Class[] { FooConfig.class }, DelegatingSmartContextLoader.class); @@ -80,21 +77,19 @@ public class ContextLoaderUtilsMergedConfigTests extends AbstractContextLoaderUt @Test public void buildMergedConfigWithLocalAnnotationAndOverriddenContextLoaderAndLocations() { - Class testClass = LocationsFoo.class; + Class testClass = PropertiesLocationsFoo.class; Class expectedContextLoaderClass = GenericPropertiesContextLoader.class; - MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, - expectedContextLoaderClass.getName(), null); + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); - assertMergedConfig(mergedConfig, testClass, new String[] { "classpath:/foo.xml" }, EMPTY_CLASS_ARRAY, + assertMergedConfig(mergedConfig, testClass, new String[] { "classpath:/foo.properties" }, EMPTY_CLASS_ARRAY, expectedContextLoaderClass); } @Test public void buildMergedConfigWithLocalAnnotationAndOverriddenContextLoaderAndClasses() { - Class testClass = ClassesFoo.class; + Class testClass = PropertiesClassesFoo.class; Class expectedContextLoaderClass = GenericPropertiesContextLoader.class; - MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, - expectedContextLoaderClass.getName(), null); + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, new Class[] { FooConfig.class }, expectedContextLoaderClass); @@ -104,8 +99,8 @@ public class ContextLoaderUtilsMergedConfigTests extends AbstractContextLoaderUt public void buildMergedConfigWithLocalAndInheritedAnnotationsAndLocations() { Class testClass = LocationsBar.class; String[] expectedLocations = new String[] { "/foo.xml", "/bar.xml" }; + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); - MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); assertMergedConfig(mergedConfig, testClass, expectedLocations, EMPTY_CLASS_ARRAY, AnnotationConfigContextLoader.class); } @@ -114,8 +109,8 @@ public class ContextLoaderUtilsMergedConfigTests extends AbstractContextLoaderUt public void buildMergedConfigWithLocalAndInheritedAnnotationsAndClasses() { Class testClass = ClassesBar.class; Class[] expectedClasses = new Class[] { FooConfig.class, BarConfig.class }; + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); - MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, AnnotationConfigContextLoader.class); } @@ -124,8 +119,8 @@ public class ContextLoaderUtilsMergedConfigTests extends AbstractContextLoaderUt public void buildMergedConfigWithAnnotationsAndOverriddenLocations() { Class testClass = OverriddenLocationsBar.class; String[] expectedLocations = new String[] { "/bar.xml" }; + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); - MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); assertMergedConfig(mergedConfig, testClass, expectedLocations, EMPTY_CLASS_ARRAY, AnnotationConfigContextLoader.class); } @@ -134,8 +129,8 @@ public class ContextLoaderUtilsMergedConfigTests extends AbstractContextLoaderUt public void buildMergedConfigWithAnnotationsAndOverriddenClasses() { Class testClass = OverriddenClassesBar.class; Class[] expectedClasses = new Class[] { BarConfig.class }; + MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); - MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass, null, null); assertMergedConfig(mergedConfig, testClass, EMPTY_STRING_ARRAY, expectedClasses, AnnotationConfigContextLoader.class); } diff --git a/spring-test/src/test/java/org/springframework/test/context/MetaAnnotationUtilsTests.java b/spring-test/src/test/java/org/springframework/test/util/MetaAnnotationUtilsTests.java similarity index 98% rename from spring-test/src/test/java/org/springframework/test/context/MetaAnnotationUtilsTests.java rename to spring-test/src/test/java/org/springframework/test/util/MetaAnnotationUtilsTests.java index 01a81d6456d..93cd0cecc91 100644 --- a/spring-test/src/test/java/org/springframework/test/context/MetaAnnotationUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/util/MetaAnnotationUtilsTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.util; import java.lang.annotation.Annotation; import java.lang.annotation.Documented; @@ -27,12 +27,15 @@ import org.junit.Test; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; -import org.springframework.test.context.MetaAnnotationUtils.AnnotationDescriptor; -import org.springframework.test.context.MetaAnnotationUtils.UntypedAnnotationDescriptor; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.util.MetaAnnotationUtils; +import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor; +import org.springframework.test.util.MetaAnnotationUtils.UntypedAnnotationDescriptor; import org.springframework.transaction.annotation.Transactional; +import static org.springframework.test.util.MetaAnnotationUtils.*; + import static org.junit.Assert.*; -import static org.springframework.test.context.MetaAnnotationUtils.*; /** * Unit tests for {@link MetaAnnotationUtils}. diff --git a/spring-test/src/test/java/org/springframework/test/context/OverriddenMetaAnnotationAttributesTests.java b/spring-test/src/test/java/org/springframework/test/util/OverriddenMetaAnnotationAttributesTests.java similarity index 95% rename from spring-test/src/test/java/org/springframework/test/context/OverriddenMetaAnnotationAttributesTests.java rename to spring-test/src/test/java/org/springframework/test/util/OverriddenMetaAnnotationAttributesTests.java index d2e1abee9f9..4b6e43eb75d 100644 --- a/spring-test/src/test/java/org/springframework/test/context/OverriddenMetaAnnotationAttributesTests.java +++ b/spring-test/src/test/java/org/springframework/test/util/OverriddenMetaAnnotationAttributesTests.java @@ -14,17 +14,20 @@ * limitations under the License. */ -package org.springframework.test.context; +package org.springframework.test.util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import org.junit.Test; import org.springframework.core.annotation.AnnotationAttributes; -import org.springframework.test.context.MetaAnnotationUtils.AnnotationDescriptor; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.util.MetaAnnotationUtils; +import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor; + +import static org.springframework.test.util.MetaAnnotationUtils.*; import static org.junit.Assert.*; -import static org.springframework.test.context.MetaAnnotationUtils.*; /** * Unit tests for {@link MetaAnnotationUtils} that verify support for overridden