From 1f93777bbd9ec10ac1693a1362231a3f2f74aa8d Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 9 Aug 2012 19:05:08 +0200 Subject: [PATCH] Support ApplicationContextInitializers in the TCF Starting with Spring 3.1 applications can specify contextInitializerClasses via context-param and init-param in web.xml; however, there is currently no way to have such initializers invoked in integration testing scenarios without writing a custom SmartContextLoader. For comprehensive integration testing it should therefore be possible to re-use ApplicationContextInitializers in the Spring TestContext Framework as well. This commit makes this possible at the @ContextConfiguration level by allowing an array of ACI types to be specified, and the out-of-the-box SmartContextLoader implementations invoke the declared initializers at the appropriate time. - Added initializers and inheritInitializers attributes to @ContextConfiguration. - Introduced support for ApplicationContextInitializers in ContextConfigurationAttributes, MergedContextConfiguration, and ContextLoaderUtils. - MergedContextConfiguration stores context initializer classes as a Set and incorporates them into the implementations of hashCode() and equals() for proper context caching. - ApplicationContextInitializers are invoked in the new prepareContext(GenericApplicationContext, MergedContextConfiguration) method in AbstractGenericContextLoader, and ordering declared via the Ordered interface and @Order annotation is honored. - Updated DelegatingSmartContextLoader to support initializers. Specifically, a test class may optionally declare neither XML configuration files nor annotated classes and instead declare only application context initializers. In such cases, an attempt will still be made to detect defaults, but their absence will not result an an exception. - Documented support for application context initializers in Javadoc and in the testing chapter of the reference manual. Issue: SPR-9011 --- spring-test/.springBeans | 3 +- .../test/context/ContextConfiguration.java | 86 ++++- .../ContextConfigurationAttributes.java | 98 +++++- .../test/context/ContextLoader.java | 4 +- .../test/context/ContextLoaderUtils.java | 108 ++++-- .../context/MergedContextConfiguration.java | 84 ++++- .../support/AbstractGenericContextLoader.java | 89 ++++- .../support/DelegatingSmartContextLoader.java | 47 ++- .../test/context/ContextLoaderUtilsTests.java | 239 ++++++++++--- .../MergedContextConfigurationTests.java | 107 +++++- .../test/context/junit4/aci/AciTestSuite.java | 51 +++ .../junit4/aci/DevProfileInitializer.java | 31 ++ .../junit4/aci/FooBarAliasInitializer.java | 31 ++ .../aci/annotation/DevProfileConfig.java | 35 ++ .../junit4/aci/annotation/GlobalConfig.java | 38 ++ ...alizerWithoutConfigFilesOrClassesTest.java | 61 ++++ ...rgedInitializersAnnotationConfigTests.java | 43 +++ ...ipleInitializersAnnotationConfigTests.java | 54 +++ ...eredInitializersAnnotationConfigTests.java | 145 ++++++++ ...ddenInitializersAnnotationConfigTests.java | 44 +++ ...ingleInitializerAnnotationConfigTests.java | 60 ++++ ...ipleInitializersXmlConfigTests-context.xml | 19 + .../MultipleInitializersXmlConfigTests.java | 53 +++ src/reference/docbook/testing.xml | 327 ++++++++++++------ 24 files changed, 1596 insertions(+), 261 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/aci/AciTestSuite.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/aci/DevProfileInitializer.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/aci/FooBarAliasInitializer.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/DevProfileConfig.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/GlobalConfig.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/InitializerWithoutConfigFilesOrClassesTest.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/MergedInitializersAnnotationConfigTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/MultipleInitializersAnnotationConfigTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/OrderedInitializersAnnotationConfigTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/OverriddenInitializersAnnotationConfigTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/aci/annotation/SingleInitializerAnnotationConfigTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/aci/xml/MultipleInitializersXmlConfigTests-context.xml create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/aci/xml/MultipleInitializersXmlConfigTests.java diff --git a/spring-test/.springBeans b/spring-test/.springBeans index 0d97a70a645..7d64f21cfb8 100644 --- a/spring-test/.springBeans +++ b/spring-test/.springBeans @@ -1,13 +1,14 @@ 1 - + src/test/java/org/springframework/test/context/junit4/profile/xml/DefaultProfileXmlConfigTests-context.xml + src/test/java/org/springframework/test/context/junit4/aci/xml/MultipleInitializersXmlConfigTests-context.xml 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 60bfa107282..4710ee7af97 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 @@ -23,19 +23,19 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; /** - * {@code ContextConfiguration} defines class-level metadata that is + * {@code @ContextConfiguration} defines class-level metadata that is * used to determine how to load and configure an * {@link org.springframework.context.ApplicationContext ApplicationContext} - * for test classes. + * for integration tests. * *

Supported Resource Types

* *

Prior to Spring 3.1, only path-based resource locations were supported. - * As of Spring 3.1, {@link #loader context loaders} may choose to support + * As of Spring 3.1, {@linkplain #loader context loaders} may choose to support * either path-based or class-based resources (but not both). Consequently * {@code @ContextConfiguration} can be used to declare either path-based * resource locations (via the {@link #locations} or {@link #value} @@ -47,16 +47,20 @@ import org.springframework.context.annotation.Configuration; *

The term annotated class can refer to any of the following. * *

* - * Consult the JavaDoc for {@link Configuration @Configuration} and {@link Bean @Bean} + * Consult the Javadoc for + * {@link org.springframework.context.annotation.Configuration @Configuration} and + * {@link org.springframework.context.annotation.Bean @Bean} * for further information regarding the configuration and semantics of * annotated classes. * @@ -66,8 +70,8 @@ import org.springframework.context.annotation.Configuration; * @see SmartContextLoader * @see ContextConfigurationAttributes * @see MergedContextConfiguration - * @see org.springframework.context.ApplicationContext * @see ActiveProfiles + * @see org.springframework.context.ApplicationContext */ @Documented @Inherited @@ -82,6 +86,7 @@ public @interface ContextConfiguration { * with {@link #locations} or {@link #classes}, but it may be used * instead of {@link #locations}. * @since 3.0 + * @see #inheritLocations */ String[] value() default {}; @@ -111,6 +116,7 @@ public @interface ContextConfiguration { * {@link #value} or {@link #classes}, but it may be used instead of * {@link #value}. * @since 2.5 + * @see #inheritLocations */ String[] locations() default {}; @@ -131,9 +137,31 @@ public @interface ContextConfiguration { * @since 3.1 * @see org.springframework.context.annotation.Configuration * @see org.springframework.test.context.support.AnnotationConfigContextLoader + * @see #inheritLocations */ Class[] classes() default {}; + /** + * The application context initializer classes to use for initializing + * a {@link ConfigurableApplicationContext}. + * + *

The concrete {@code ConfigurableApplicationContext} type supported by each + * declared initializer must be compatible with the type of {@code ApplicationContext} + * created by the {@link SmartContextLoader} in use. + * + *

{@code SmartContextLoader} implementations typically detect whether + * Spring's {@link org.springframework.core.Ordered Ordered} interface has been + * implemented or if the @{@link org.springframework.core.annotation.Order Order} + * annotation is present and sort instances accordingly prior to invoking them. + * + * @since 3.2 + * @see org.springframework.context.ApplicationContextInitializer + * @see org.springframework.context.ConfigurableApplicationContext + * @see #inheritInitializers + * @see #loader + */ + Class>[] initializers() default {}; + /** * Whether or not {@link #locations resource locations} or annotated * classes from test superclasses should be inherited. @@ -194,7 +222,45 @@ public @interface ContextConfiguration { boolean inheritLocations() default true; /** - * The type of {@link ContextLoader} (or {@link SmartContextLoader}) to use + * Whether or not {@linkplain #initializers context initializers} from test + * superclasses should be inherited. + * + *

The default value is true. This means that an annotated + * class will inherit the application context initializers defined + * by test superclasses. Specifically, the initializers for a given test + * class will be added to the set of initializers defined by test + * superclasses. Thus, subclasses have the option of extending the + * set of initializers. + * + *

If inheritInitializers is set to false, the + * initializers for the annotated class will shadow and effectively + * replace any initializers defined by superclasses. + * + *

In the following example, the + * {@link org.springframework.context.ApplicationContext ApplicationContext} + * for {@code ExtendedTest} will be initialized using + * {@code BaseInitializer} and {@code ExtendedInitializer}. + * Note, however, that the order in which the initializers are invoked + * depends on whether they implement {@link org.springframework.core.Ordered + * Ordered} or are annotated with {@link org.springframework.core.annotation.Order + * @Order}. + *

+	 * @ContextConfiguration(initializers = BaseInitializer.class)
+	 * public class BaseTest {
+	 *     // ...
+	 * }
+	 * 
+	 * @ContextConfiguration(initializers = ExtendedInitializer.class)
+	 * public class ExtendedTest extends BaseTest {
+	 *     // ...
+	 * }
+	 * 
+ * @since 3.2 + */ + boolean inheritInitializers() default true; + + /** + * The type of {@link SmartContextLoader} (or {@link ContextLoader}) to use * for loading an {@link org.springframework.context.ApplicationContext * ApplicationContext}. * diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java index b7983030cef..5a481c190de 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java @@ -18,12 +18,15 @@ package org.springframework.test.context; 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.style.ToStringCreator; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** - * ContextConfigurationAttributes encapsulates the context + * {@code ContextConfigurationAttributes} encapsulates the context * configuration attributes declared on a test class via * {@link ContextConfiguration @ContextConfiguration}. * @@ -47,6 +50,10 @@ public class ContextConfigurationAttributes { private final Class contextLoaderClass; + private final Class>[] initializers; + + private final boolean inheritInitializers; + /** * Resolve resource locations from the {@link ContextConfiguration#locations() locations} @@ -68,8 +75,7 @@ public class ContextConfigurationAttributes { ObjectUtils.nullSafeToString(valueLocations), ObjectUtils.nullSafeToString(locations)); logger.error(msg); throw new IllegalStateException(msg); - } - else if (!ObjectUtils.isEmpty(valueLocations)) { + } else if (!ObjectUtils.isEmpty(valueLocations)) { locations = valueLocations; } @@ -79,31 +85,59 @@ public class ContextConfigurationAttributes { /** * Construct a new {@link ContextConfigurationAttributes} instance for the * supplied {@link ContextConfiguration @ContextConfiguration} annotation and - * the {@link Class test class} that declared it. + * the {@linkplain Class test class} that declared it. * @param declaringClass the test class that declared {@code @ContextConfiguration} * @param contextConfiguration the annotation from which to retrieve the attributes */ public ContextConfigurationAttributes(Class declaringClass, ContextConfiguration contextConfiguration) { this(declaringClass, resolveLocations(declaringClass, contextConfiguration), contextConfiguration.classes(), - contextConfiguration.inheritLocations(), contextConfiguration.loader()); + contextConfiguration.inheritLocations(), contextConfiguration.initializers(), + contextConfiguration.inheritInitializers(), contextConfiguration.loader()); } /** * Construct a new {@link ContextConfigurationAttributes} instance for the - * {@link Class test class} that declared the + * {@linkplain Class test class} that declared the * {@link ContextConfiguration @ContextConfiguration} annotation and its * corresponding attributes. * * @param declaringClass the test class that declared {@code @ContextConfiguration} * @param locations the resource locations declared via {@code @ContextConfiguration} * @param classes the annotated classes declared via {@code @ContextConfiguration} - * @param inheritLocations the inheritLocations flag declared via {@code @ContextConfiguration} + * @param inheritLocations the {@code inheritLocations} flag declared via {@code @ContextConfiguration} * @param contextLoaderClass the {@code ContextLoader} class declared via {@code @ContextConfiguration} * @throws IllegalArgumentException if the {@code declaringClass} or {@code contextLoaderClass} is - * null, or if the {@code locations} and {@code classes} are both non-empty + * {@code null}, or if the {@code locations} and {@code classes} are both non-empty + * @deprecated as of Spring 3.2, use + * {@link #ContextConfigurationAttributes(Class, String[], Class[], boolean, Class[], boolean, Class)} + * instead */ + @Deprecated public ContextConfigurationAttributes(Class declaringClass, String[] locations, Class[] classes, boolean inheritLocations, Class contextLoaderClass) { + this(declaringClass, locations, classes, inheritLocations, null, true, contextLoaderClass); + } + + /** + * Construct a new {@link ContextConfigurationAttributes} instance for the + * {@linkplain Class test class} that declared the + * {@link ContextConfiguration @ContextConfiguration} annotation and its + * corresponding attributes. + * + * @param declaringClass the test class that declared {@code @ContextConfiguration} + * @param locations the resource locations declared via {@code @ContextConfiguration} + * @param classes the annotated classes declared via {@code @ContextConfiguration} + * @param inheritLocations the {@code inheritLocations} flag declared via {@code @ContextConfiguration} + * @param initializers the context initializers declared via {@code @ContextConfiguration} + * @param inheritInitializers the {@code inheritInitializers} flag declared via {@code @ContextConfiguration} + * @param contextLoaderClass the {@code ContextLoader} class declared via {@code @ContextConfiguration} + * @throws IllegalArgumentException if the {@code declaringClass} or {@code contextLoaderClass} is + * {@code null}, or if the {@code locations} and {@code classes} are both non-empty + */ + public ContextConfigurationAttributes(Class declaringClass, String[] locations, Class[] classes, + boolean inheritLocations, + Class>[] initializers, + boolean inheritInitializers, Class contextLoaderClass) { Assert.notNull(declaringClass, "declaringClass must not be null"); Assert.notNull(contextLoaderClass, "contextLoaderClass must not be null"); @@ -122,14 +156,16 @@ public class ContextConfigurationAttributes { this.locations = locations; this.classes = classes; this.inheritLocations = inheritLocations; + this.initializers = initializers; + this.inheritInitializers = inheritInitializers; this.contextLoaderClass = contextLoaderClass; } /** - * Get the {@link Class class} that declared the + * Get the {@linkplain Class class} that declared the * {@link ContextConfiguration @ContextConfiguration} annotation. * - * @return the declaring class; never null + * @return the declaring class; never {@code null} */ public Class getDeclaringClass() { return declaringClass; @@ -143,7 +179,7 @@ public class ContextConfigurationAttributes { * represent a processed value that does not match the original value * declared via {@link ContextConfiguration @ContextConfiguration}. * - * @return the resource locations; potentially null or empty + * @return the resource locations; potentially {@code null} or empty * @see ContextConfiguration#value * @see ContextConfiguration#locations * @see #setLocations(String[]) @@ -170,7 +206,7 @@ public class ContextConfigurationAttributes { * represent a processed value that does not match the original value * declared via {@link ContextConfiguration @ContextConfiguration}. * - * @return the annotated classes; potentially null or empty + * @return the annotated classes; potentially {@code null} or empty * @see ContextConfiguration#classes * @see #setClasses(Class[]) */ @@ -192,7 +228,7 @@ public class ContextConfigurationAttributes { * Determine if this {@code ContextConfigurationAttributes} instance has * path-based resource locations. * - * @return true if the {@link #getLocations() locations} array is not empty + * @return {@code true} if the {@link #getLocations() locations} array is not empty * @see #hasResources() * @see #hasClasses() */ @@ -204,7 +240,7 @@ public class ContextConfigurationAttributes { * Determine if this {@code ContextConfigurationAttributes} instance has * class-based resources. * - * @return true if the {@link #getClasses() classes} array is not empty + * @return {@code true} if the {@link #getClasses() classes} array is not empty * @see #hasResources() * @see #hasLocations() */ @@ -216,7 +252,7 @@ public class ContextConfigurationAttributes { * Determine if this {@code ContextConfigurationAttributes} instance has * either path-based resource locations or class-based resources. * - * @return true if either the {@link #getLocations() locations} + * @return {@code true} if either the {@link #getLocations() locations} * or the {@link #getClasses() classes} array is not empty * @see #hasLocations() * @see #hasClasses() @@ -226,10 +262,10 @@ public class ContextConfigurationAttributes { } /** - * Get the inheritLocations flag that was declared via + * Get the {@code inheritLocations} flag that was declared via * {@link ContextConfiguration @ContextConfiguration}. * - * @return the inheritLocations flag + * @return the {@code inheritLocations} flag * @see ContextConfiguration#inheritLocations */ public boolean isInheritLocations() { @@ -237,10 +273,32 @@ public class ContextConfigurationAttributes { } /** - * Get the ContextLoader class that was declared via + * Get the {@code ApplicationContextInitializer} classes that were declared via + * {@link ContextConfiguration @ContextConfiguration}. + * + * @return the {@code ApplicationContextInitializer} classes + * @since 3.2 + */ + public Class>[] getInitializers() { + return initializers; + } + + /** + * Get the {@code inheritInitializers} flag that was declared via + * {@link ContextConfiguration @ContextConfiguration}. + * + * @return the {@code inheritInitializers} flag + * @since 3.2 + */ + public boolean isInheritInitializers() { + return inheritInitializers; + } + + /** + * Get the {@code ContextLoader} class that was declared via * {@link ContextConfiguration @ContextConfiguration}. * - * @return the ContextLoader class + * @return the {@code ContextLoader} class * @see ContextConfiguration#loader */ public Class getContextLoaderClass() { @@ -258,6 +316,8 @@ public class ContextConfigurationAttributes { .append("locations", ObjectUtils.nullSafeToString(locations))// .append("classes", ObjectUtils.nullSafeToString(classes))// .append("inheritLocations", inheritLocations)// + .append("initializers", ObjectUtils.nullSafeToString(initializers))// + .append("inheritInitializers", inheritInitializers)// .append("contextLoaderClass", contextLoaderClass.getName())// .toString(); } diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/ContextLoader.java index 12fdf6142a3..f28364a9b4b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextLoader.java @@ -23,8 +23,8 @@ import org.springframework.context.ApplicationContext; * for an integration test managed by the Spring TestContext Framework. * *

Note: as of Spring 3.1, implement {@link SmartContextLoader} instead - * of this interface in order to provide support for annotated classes and active - * bean definition profiles. + * of this interface in order to provide support for annotated classes, active + * bean definition profiles, and application context initializers. * *

Clients of a ContextLoader should call * {@link #processLocations(Class,String...) processLocations()} prior to 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 index 8f62aecbe5c..a5ad3516f05 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextLoaderUtils.java @@ -21,12 +21,15 @@ import static org.springframework.core.annotation.AnnotationUtils.findAnnotation import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; 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.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -156,8 +159,7 @@ abstract class ContextLoaderUtils { } return (Class) ContextLoaderUtils.class.getClassLoader().loadClass( defaultContextLoaderClassName); - } - catch (ClassNotFoundException ex) { + } catch (ClassNotFoundException ex) { throw new IllegalStateException("Could not load default ContextLoader class [" + defaultContextLoaderClassName + "]. Specify @ContextConfiguration's 'loader' " + "attribute or make the default loader class available."); @@ -169,17 +171,15 @@ abstract class ContextLoaderUtils { * attributes} for the supplied {@link Class class} and its superclasses. * *

Note that the {@link ContextConfiguration#inheritLocations - * inheritLocations} flag of {@link ContextConfiguration - * @ContextConfiguration} will be taken into consideration. - * Specifically, if the inheritLocations flag is set to - * true, configuration attributes defined in the test - * class will be appended to the configuration attributes defined in - * superclasses. + * 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 clazz the class for which to resolve the configuration attributes (must * not be null) - * @return the list of configuration attributes for the specified class, - * including configuration attributes from superclasses if appropriate + * @return the list of configuration attributes for the specified class * (never null) * @throws IllegalArgumentException if the supplied class is null or * if {@code @ContextConfiguration} is not present on the supplied class @@ -211,13 +211,69 @@ abstract class ContextLoaderUtils { attributesList.add(0, attributes); - declaringClass = contextConfiguration.inheritLocations() ? findAnnotationDeclaringClass(annotationType, - declaringClass.getSuperclass()) : null; + declaringClass = findAnnotationDeclaringClass(annotationType, declaringClass.getSuperclass()); } return attributesList; } + /** + * Create a copy of the supplied list of {@code ContextConfigurationAttributes} + * in reverse order. + * + * @since 3.2 + */ + private static List reverseContextConfigurationAttributes( + List configAttributesList) { + List configAttributesListReversed = new ArrayList( + configAttributesList); + Collections.reverse(configAttributesListReversed); + return configAttributesListReversed; + } + + /** + * 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 inheritInitializers flag is + * set to 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 null) + * @return the list of merged context initializer classes, including those + * from superclasses if appropriate (never null) + * @since 3.2 + */ + static Set>> resolveInitializerClasses( + List configAttributesList) { + Assert.notNull(configAttributesList, "configAttributesList must not be null"); + + final Set>> initializerClasses = // + new HashSet>>(); + + // Traverse config attributes in reverse order (i.e., as if we were traversing up + // the class hierarchy). + for (ContextConfigurationAttributes configAttributes : reverseContextConfigurationAttributes(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}. * @@ -266,8 +322,7 @@ abstract class ContextLoaderUtils { ObjectUtils.nullSafeToString(valueProfiles), ObjectUtils.nullSafeToString(profiles)); logger.error(msg); throw new IllegalStateException(msg); - } - else if (!ObjectUtils.isEmpty(valueProfiles)) { + } else if (!ObjectUtils.isEmpty(valueProfiles)) { profiles = valueProfiles; } @@ -309,31 +364,38 @@ abstract class ContextLoaderUtils { final List locationsList = new ArrayList(); final List> classesList = new ArrayList>(); - for (ContextConfigurationAttributes configAttributes : configAttributesList) { + // Traverse config attributes in reverse order (i.e., as if we were traversing up + // the class hierarchy). + for (ContextConfigurationAttributes configAttributes : reverseContextConfigurationAttributes(configAttributesList)) { if (logger.isTraceEnabled()) { - logger.trace(String.format( - "Processing locations and classes for context configuration attributes [%s]", configAttributes)); + 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(Arrays.asList(configAttributes.getLocations())); - classesList.addAll(Arrays.asList(configAttributes.getClasses())); - } - else { + 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(Arrays.asList(processedLocations)); + 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); - return new MergedContextConfiguration(testClass, locations, classes, activeProfiles, contextLoader); + return new MergedContextConfiguration(testClass, locations, classes, initializerClasses, activeProfiles, + contextLoader); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java index 274d5da8c43..e484253b8c3 100644 --- a/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java @@ -18,9 +18,13 @@ package org.springframework.test.context; import java.io.Serializable; import java.util.Arrays; +import java.util.Collections; +import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.style.ToStringCreator; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -59,10 +63,13 @@ public class MergedContextConfiguration implements Serializable { private static final String[] EMPTY_STRING_ARRAY = new String[0]; private static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; + private static final Set>> EMPTY_INITIALIZER_CLASSES = // + Collections.>> emptySet(); private final Class testClass; private final String[] locations; private final Class[] classes; + private final Set>> contextInitializerClasses; private final String[] activeProfiles; private final ContextLoader contextLoader; @@ -75,6 +82,12 @@ public class MergedContextConfiguration implements Serializable { return classes == null ? EMPTY_CLASS_ARRAY : classes; } + private static Set>> processContextInitializerClasses( + Set>> contextInitializerClasses) { + return contextInitializerClasses == null ? EMPTY_INITIALIZER_CLASSES + : Collections.unmodifiableSet(contextInitializerClasses); + } + private static String[] processActiveProfiles(String[] activeProfiles) { if (activeProfiles == null) { return EMPTY_STRING_ARRAY; @@ -111,46 +124,84 @@ public class MergedContextConfiguration implements Serializable { * @param classes the merged annotated classes * @param activeProfiles the merged active bean definition profiles * @param contextLoader the resolved ContextLoader + * @see #MergedContextConfiguration(Class, String[], Class[], Set, String[], ContextLoader) */ public MergedContextConfiguration(Class testClass, String[] locations, Class[] classes, String[] activeProfiles, ContextLoader contextLoader) { + this(testClass, locations, classes, null, activeProfiles, contextLoader); + } + + /** + * Create a new {@code MergedContextConfiguration} instance for the + * supplied test class, resource locations, annotated classes, context + * initializers, active profiles, and {@code ContextLoader}. + * + *

If a null value is supplied for locations, + * classes, or activeProfiles an empty array will + * be stored instead. If a null value is supplied for the + * contextInitializerClasses an empty set will be stored instead. + * Furthermore, active profiles will be sorted, and duplicate profiles will + * be removed. + * + * @param testClass the test class for which the configuration was merged + * @param locations the merged resource locations + * @param classes the merged annotated classes + * @param contextInitializerClasses the merged context initializer classes + * @param activeProfiles the merged active bean definition profiles + * @param contextLoader the resolved ContextLoader + */ + public MergedContextConfiguration( + Class testClass, + String[] locations, + Class[] classes, + Set>> contextInitializerClasses, + String[] activeProfiles, ContextLoader contextLoader) { this.testClass = testClass; this.locations = processLocations(locations); this.classes = processClasses(classes); + this.contextInitializerClasses = processContextInitializerClasses(contextInitializerClasses); this.activeProfiles = processActiveProfiles(activeProfiles); this.contextLoader = contextLoader; } /** - * Get the {@link Class test class} associated with this {@code MergedContextConfiguration}. + * Get the {@linkplain Class test class} associated with this {@code MergedContextConfiguration}. */ public Class getTestClass() { return testClass; } /** - * Get the merged resource locations for the {@link #getTestClass() test class}. + * Get the merged resource locations for the {@linkplain #getTestClass() test class}. */ public String[] getLocations() { return locations; } /** - * Get the merged annotated classes for the {@link #getTestClass() test class}. + * Get the merged annotated classes for the {@linkplain #getTestClass() test class}. */ public Class[] getClasses() { return classes; } /** - * Get the merged active bean definition profiles for the {@link #getTestClass() test class}. + * Get the merged {@code ApplicationContextInitializer} classes for the + * {@linkplain #getTestClass() test class}. + */ + public Set>> getContextInitializerClasses() { + return contextInitializerClasses; + } + + /** + * Get the merged active bean definition profiles for the {@linkplain #getTestClass() test class}. */ public String[] getActiveProfiles() { return activeProfiles; } /** - * Get the resolved {@link ContextLoader} for the {@link #getTestClass() test class}. + * Get the resolved {@link ContextLoader} for the {@linkplain #getTestClass() test class}. */ public ContextLoader getContextLoader() { return contextLoader; @@ -159,7 +210,7 @@ public class MergedContextConfiguration implements Serializable { /** * Generate a unique hash code for all properties of this * {@code MergedContextConfiguration} excluding the - * {@link #getTestClass() test class}. + * {@linkplain #getTestClass() test class}. */ @Override public int hashCode() { @@ -167,6 +218,7 @@ public class MergedContextConfiguration implements Serializable { int result = 1; result = prime * result + Arrays.hashCode(locations); result = prime * result + Arrays.hashCode(classes); + result = prime * result + contextInitializerClasses.hashCode(); result = prime * result + Arrays.hashCode(activeProfiles); result = prime * result + nullSafeToString(contextLoader).hashCode(); return result; @@ -174,10 +226,11 @@ public class MergedContextConfiguration implements Serializable { /** * Determine if the supplied object is equal to this {@code MergedContextConfiguration} - * instance by comparing both object's {@link #getLocations() locations}, - * {@link #getClasses() annotated classes}, {@link #getActiveProfiles() - * active profiles}, and the fully qualified names of their - * {@link #getContextLoader() ContextLoaders}. + * instance by comparing both object's {@linkplain #getLocations() locations}, + * {@linkplain #getClasses() annotated classes}, + * {@linkplain #getContextInitializerClasses() context initializer classes}, + * {@linkplain #getActiveProfiles() active profiles}, and the fully qualified + * names of their {@link #getContextLoader() ContextLoaders}. */ @Override public boolean equals(Object obj) { @@ -197,6 +250,9 @@ public class MergedContextConfiguration implements Serializable { if (!Arrays.equals(this.classes, that.classes)) { return false; } + if (!this.contextInitializerClasses.equals(that.contextInitializerClasses)) { + return false; + } if (!Arrays.equals(this.activeProfiles, that.activeProfiles)) { return false; } @@ -208,9 +264,10 @@ public class MergedContextConfiguration implements Serializable { } /** - * Provide a String representation of the {@link #getTestClass() test class}, - * {@link #getLocations() locations}, {@link #getClasses() annotated classes}, - * {@link #getActiveProfiles() active profiles}, and the name of the + * Provide a String representation of the {@linkplain #getTestClass() test class}, + * {@linkplain #getLocations() locations}, {@linkplain #getClasses() annotated classes}, + * {@linkplain #getContextInitializerClasses() context initializer classes}, + * {@linkplain #getActiveProfiles() active profiles}, and the name of the * {@link #getContextLoader() ContextLoader}. */ @Override @@ -219,6 +276,7 @@ public class MergedContextConfiguration implements Serializable { .append("testClass", testClass)// .append("locations", ObjectUtils.nullSafeToString(locations))// .append("classes", ObjectUtils.nullSafeToString(classes))// + .append("contextInitializerClasses", ObjectUtils.nullSafeToString(contextInitializerClasses))// .append("activeProfiles", ObjectUtils.nullSafeToString(activeProfiles))// .append("contextLoader", nullSafeToString(contextLoader))// .toString(); diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java index e1669ac176b..06145c6e94b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java @@ -16,14 +16,25 @@ package org.springframework.test.context.support; +import java.util.ArrayList; +import java.util.Collections; +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.beans.factory.support.BeanDefinitionReader; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -66,11 +77,8 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader * *