From 35c372f200258f74721a82441a03313ee21fb315 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 21 Jul 2014 23:29:05 +0200 Subject: [PATCH] Support Groovy scripts in the TCF Spring Framework 4.0 introduced first-class support for a Groovy-based DSL for defining the beans for an ApplicationContext. However, prior to this commit, the Spring TestContext Framework (TCF) did not provide any out-of-the-box support for using Groovy scripts as path-based resource locations when loading an application context for tests. This commit addresses this issue by introducing first-class support for using Groovy scripts to load the ApplicationContext for integration tests managed by the TCF. Specifically, the following changes have been made in the TCF to support Groovy scripts. - Introduced getResourceSuffixes() in AbstractContextLoader in order to support multiple resource suffixes in the default detection process. This feature is used by the new Groovy/Xml context loaders. - Introduced GenericGroovyXmlContextLoader and GenericGroovyXmlWebContextLoader which support both Groovy scripts and XML config files for loading bean definitions. Furthermore, these loaders support "-context.xml" and "Context.groovy" as resource suffixes when detecting defaults. Note that a default XML config file will be detected before a default Groovy script. - DelegatingSmartContextLoader and WebDelegatingSmartContextLoader now use reflection to choose between using GenericGroovyXmlContextLoader and GenericGroovyXmlWebContextLoader vs. GenericXmlContextLoader and GenericXmlWebContextLoader as their XML loaders, depending on whether Groovy is present in the classpath. - Groovy scripts can be configured via the 'locations' or 'value' attributes of @ContextConfiguration and can be mixed seamlessly with XML config files. Issue: SPR-11233 --- build.gradle | 1 + .../test/context/ContextConfiguration.java | 99 +++++++------- .../support/AbstractContextLoader.java | 98 +++++++++----- .../AbstractDelegatingSmartContextLoader.java | 42 +++--- .../AnnotationConfigContextLoader.java | 2 + .../support/DelegatingSmartContextLoader.java | 38 +++++- .../GenericGroovyXmlContextLoader.java | 69 ++++++++++ .../support/GenericXmlContextLoader.java | 14 +- .../web/AnnotationConfigWebContextLoader.java | 2 + .../web/GenericGroovyXmlWebContextLoader.java | 69 ++++++++++ .../web/GenericXmlWebContextLoader.java | 8 +- .../web/WebDelegatingSmartContextLoader.java | 37 +++++- .../AbsolutePathGroovySpringContextTests.java | 35 +++++ ...riptDetectionGroovySpringContextTests.java | 63 +++++++++ ...tionGroovySpringContextTestsContext.groovy | 40 ++++++ ...rsedesGroovySpringContextTests-context.xml | 9 ++ ...XmlSupersedesGroovySpringContextTests.java | 47 +++++++ ...edesGroovySpringContextTestsContext.groovy | 30 +++++ .../groovy/GroovyControlGroupTests.java | 63 +++++++++ .../groovy/GroovySpringContextTests.java | 122 ++++++++++++++++++ .../MixedXmlAndGroovySpringContextTests.java | 66 ++++++++++ .../RelativePathGroovySpringContextTests.java | 35 +++++ .../test/context/groovy/context.groovy | 40 ++++++ .../test/context/groovy/contextA.groovy | 30 +++++ .../test/context/groovy/contextB.xml | 19 +++ .../test/context/web/BasicGroovyWacTests.java | 38 ++++++ .../web/BasicGroovyWacTestsContext.groovy | 3 + .../src/test/resources/log4j.properties | 3 +- 28 files changed, 1011 insertions(+), 111 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/context/support/GenericGroovyXmlContextLoader.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/web/GenericGroovyXmlWebContextLoader.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/groovy/AbsolutePathGroovySpringContextTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/groovy/DefaultScriptDetectionGroovySpringContextTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/groovy/DefaultScriptDetectionGroovySpringContextTestsContext.groovy create mode 100644 spring-test/src/test/java/org/springframework/test/context/groovy/DefaultScriptDetectionXmlSupersedesGroovySpringContextTests-context.xml create mode 100644 spring-test/src/test/java/org/springframework/test/context/groovy/DefaultScriptDetectionXmlSupersedesGroovySpringContextTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/groovy/DefaultScriptDetectionXmlSupersedesGroovySpringContextTestsContext.groovy create mode 100644 spring-test/src/test/java/org/springframework/test/context/groovy/GroovyControlGroupTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/groovy/GroovySpringContextTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/groovy/MixedXmlAndGroovySpringContextTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/groovy/RelativePathGroovySpringContextTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/groovy/context.groovy create mode 100644 spring-test/src/test/java/org/springframework/test/context/groovy/contextA.groovy create mode 100644 spring-test/src/test/java/org/springframework/test/context/groovy/contextB.xml create mode 100644 spring-test/src/test/java/org/springframework/test/context/web/BasicGroovyWacTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/web/BasicGroovyWacTestsContext.groovy diff --git a/build.gradle b/build.gradle index 91ea5725e2..715f1c5c3e 100644 --- a/build.gradle +++ b/build.gradle @@ -929,6 +929,7 @@ project("spring-test") { optional("javax.portlet:portlet-api:2.0") optional("javax.el:javax.el-api:2.2.5") optional("org.aspectj:aspectjweaver:${aspectjVersion}") + optional("org.codehaus.groovy:groovy-all:${groovyVersion}") optional("org.hamcrest:hamcrest-core:1.3") optional("com.jayway.jsonpath:json-path:0.9.0") optional("org.skyscreamer:jsonassert:1.2.3") 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 a716284a90..f298dc787c 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 @@ -27,48 +27,52 @@ import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; /** - * {@code @ContextConfiguration} defines class-level metadata that is - * used to determine how to load and configure an - * {@link org.springframework.context.ApplicationContext ApplicationContext} - * for integration tests. + * {@code @ContextConfiguration} defines class-level metadata that is used to determine + * how to load and configure an {@link org.springframework.context.ApplicationContext + * ApplicationContext} for integration tests. * *

Supported Resource Types

* - *

Prior to Spring 3.1, only path-based resource locations were supported. - * As of Spring 3.1, {@linkplain #loader context loaders} may choose to support - * either path-based or class-based resources. As of Spring - * 4.0.4, {@linkplain #loader context loaders} may choose to support path-based + *

+ * Prior to Spring 3.1, only path-based resource locations (typically XML configuration + * files) were supported. As of Spring 3.1, {@linkplain #loader context loaders} may + * choose to support either path-based or class-based resources. As of + * Spring 4.0.4, {@linkplain #loader context loaders} may choose to support path-based * and class-based resources simultaneously. Consequently - * {@code @ContextConfiguration} can be used to declare either path-based - * resource locations (via the {@link #locations} or {@link #value} attribute) - * or annotated classes (via the {@link #classes} attribute). Note, - * however, that most implementations of {@link SmartContextLoader} only support - * a single resource type. + * {@code @ContextConfiguration} can be used to declare either path-based resource + * locations (via the {@link #locations} or {@link #value} attribute) or + * annotated classes (via the {@link #classes} attribute). Note, however, that most + * implementations of {@link SmartContextLoader} only support a single resource type. As + * of Spring 4.1, path-based resource locations may be either XML configuration files or + * Groovy scripts (if Groovy is on the classpath). Of course, third-party frameworks may + * choose to support additional types of path-based resources. * *

Annotated Classes

* - *

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

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

* - *

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 + *

+ * 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. * - *

As of Spring Framework 4.0, this annotation may be used as a - * meta-annotation to create custom composed annotations. + *

+ * As of Spring Framework 4.0, this annotation may be used as a meta-annotation + * to create custom composed annotations. * * @author Sam Brannen * @since 2.5 @@ -104,20 +108,20 @@ public @interface ContextConfiguration { *

Check out the Javadoc for * {@link org.springframework.test.context.support.AbstractContextLoader#modifyLocations * AbstractContextLoader.modifyLocations()} for details on how a location - * String will be interpreted at runtime, in particular in case of a relative + * will be interpreted at runtime, in particular in case of a relative * path. Also, check out the documentation on * {@link org.springframework.test.context.support.AbstractContextLoader#generateDefaultLocations - * AbstractContextLoader.generateDefaultLocations()} for details on the default - * locations that are going to be used if none are specified. + * AbstractContextLoader.generateDefaultLocations()} for details on the + * default locations that are going to be used if none are specified. * - *

Note that the above-mentioned default rules only apply for a standard + *

Note that the aforementioned default rules only apply for a standard * {@link org.springframework.test.context.support.AbstractContextLoader * AbstractContextLoader} subclass such as - * {@link org.springframework.test.context.support.GenericXmlContextLoader - * GenericXmlContextLoader} which is the effective default implementation - * used at runtime if {@code locations} are configured. See the - * documentation for {@link #loader} for further details regarding default - * loaders. + * {@link org.springframework.test.context.support.GenericXmlContextLoader GenericXmlContextLoader} or + * {@link org.springframework.test.context.support.GenericGroovyXmlContextLoader GenericGroovyXmlContextLoader} + * which are the effective default implementations used at runtime if + * {@code locations} are configured. See the documentation for {@link #loader} + * for further details regarding default loaders. * *

This attribute may not be used in conjunction with * {@link #value}, but it may be used instead of {@link #value}. @@ -186,17 +190,17 @@ public @interface ContextConfiguration { *

In the following example that uses path-based resource locations, the * {@link org.springframework.context.ApplicationContext ApplicationContext} * for {@code ExtendedTest} will be loaded from - * "base-context.xml" and - * "extended-context.xml", in that order. Beans defined in - * "extended-context.xml" may therefore override those defined in - * "base-context.xml". + * {@code "base-context.xml"} and + * {@code "extended-context.xml"}, in that order. Beans defined in + * {@code "extended-context.xml"} may therefore override those defined + * in {@code "base-context.xml"}. *

-	 * @ContextConfiguration("base-context.xml")
+	 * @ContextConfiguration("base-context.xml")
 	 * public class BaseTest {
 	 *     // ...
 	 * }
 	 *
-	 * @ContextConfiguration("extended-context.xml")
+	 * @ContextConfiguration("extended-context.xml")
 	 * public class ExtendedTest extends BaseTest {
 	 *     // ...
 	 * }
@@ -281,16 +285,13 @@ public @interface ContextConfiguration {
 	 * {@link org.springframework.test.context.web.WebAppConfiguration
 	 * @WebAppConfiguration}. For further details on the default behavior
 	 * of various concrete {@code SmartContextLoaders}, check out the Javadoc for
-	 * {@link org.springframework.test.context.support.AbstractContextLoader
-	 * AbstractContextLoader},
-	 * {@link org.springframework.test.context.support.GenericXmlContextLoader
-	 * GenericXmlContextLoader},
-	 * {@link org.springframework.test.context.support.AnnotationConfigContextLoader
-	 * AnnotationConfigContextLoader},
-	 * {@link org.springframework.test.context.web.GenericXmlWebContextLoader
-	 * GenericXmlWebContextLoader}, and
-	 * {@link org.springframework.test.context.web.AnnotationConfigWebContextLoader
-	 * AnnotationConfigWebContextLoader}.
+	 * {@link org.springframework.test.context.support.AbstractContextLoader AbstractContextLoader},
+	 * {@link org.springframework.test.context.support.GenericXmlContextLoader GenericXmlContextLoader},
+	 * {@link org.springframework.test.context.support.GenericGroovyXmlContextLoader GenericGroovyXmlContextLoader},
+	 * {@link org.springframework.test.context.support.AnnotationConfigContextLoader AnnotationConfigContextLoader},
+	 * {@link org.springframework.test.context.web.GenericXmlWebContextLoader GenericXmlWebContextLoader},
+	 * {@link org.springframework.test.context.web.GenericGroovyXmlWebContextLoader GenericGroovyXmlWebContextLoader}, and
+	 * {@link org.springframework.test.context.web.AnnotationConfigWebContextLoader AnnotationConfigWebContextLoader}.
 	 *
 	 * @since 2.5
 	 */
diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java
index e21f4e5e75..23181b8a88 100644
--- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java
+++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java
@@ -55,6 +55,7 @@ import org.springframework.util.ResourceUtils;
  * @author Juergen Hoeller
  * @since 2.5
  * @see #generateDefaultLocations
+ * @see #getResourceSuffixes
  * @see #modifyLocations
  */
 public abstract class AbstractContextLoader implements SmartContextLoader {
@@ -144,14 +145,14 @@ public abstract class AbstractContextLoader implements SmartContextLoader {
 	// --- ContextLoader -------------------------------------------------------
 
 	/**
-	 * If the supplied {@code locations} are {@code null} or
-	 * empty and {@link #isGenerateDefaultLocations()} returns
-	 * {@code true}, default locations will be
-	 * {@link #generateDefaultLocations(Class) generated} for the specified
-	 * {@link Class class} and the configured
-	 * {@link #getResourceSuffix() resource suffix}; otherwise, the supplied
-	 * {@code locations} will be {@link #modifyLocations modified} if
-	 * necessary and returned.
+	 * If the supplied {@code locations} are {@code null} or empty
+	 * and {@link #isGenerateDefaultLocations()} returns {@code true},
+	 * default locations will be {@link #generateDefaultLocations(Class)
+	 * generated} (i.e., detected) for the specified {@link Class class}
+	 * and the configured {@linkplain #getResourceSuffixes() resource suffixes};
+	 * otherwise, the supplied {@code locations} will be
+	 * {@linkplain #modifyLocations modified} if necessary and returned.
+	 *
 	 * @param clazz the class with which the locations are associated: to be
 	 * used when generating default locations
 	 * @param locations the unmodified locations to use for loading the
@@ -173,45 +174,57 @@ public abstract class AbstractContextLoader implements SmartContextLoader {
 	/**
 	 * Generate the default classpath resource locations array based on the
 	 * supplied class.
+	 *
 	 * 

For example, if the supplied class is {@code com.example.MyTest}, * the generated locations will contain a single string with a value of - * "classpath:com/example/MyTest{@code }", - * where {@code } is the value of the - * {@link #getResourceSuffix() resource suffix} string. + * {@code "classpath:com/example/MyTest"}, where {@code } + * is the value of the first configured + * {@linkplain #getResourceSuffixes() resource suffix} for which the + * generated location actually exists in the classpath. + * *

As of Spring 3.1, the implementation of this method adheres to the * contract defined in the {@link SmartContextLoader} SPI. Specifically, * this method will preemptively verify that the generated default * location actually exists. If it does not exist, this method will log a * warning and return an empty array. + * *

Subclasses can override this method to implement a different * default location generation strategy. + * * @param clazz the class for which the default locations are to be generated * @return an array of default application context resource locations * @since 2.5 - * @see #getResourceSuffix() + * @see #getResourceSuffixes() */ protected String[] generateDefaultLocations(Class clazz) { Assert.notNull(clazz, "Class must not be null"); - String suffix = getResourceSuffix(); - Assert.hasText(suffix, "Resource suffix must not be empty"); - String resourcePath = ClassUtils.convertClassNameToResourcePath(clazz.getName()) + suffix; - String prefixedResourcePath = ResourceUtils.CLASSPATH_URL_PREFIX + resourcePath; - ClassPathResource classPathResource = new ClassPathResource(resourcePath); - if (classPathResource.exists()) { - if (logger.isInfoEnabled()) { - logger.info(String.format("Detected default resource location \"%s\" for test class [%s]", - prefixedResourcePath, clazz.getName())); + String[] suffixes = getResourceSuffixes(); + for (String suffix : suffixes) { + Assert.hasText(suffix, "Resource suffix must not be empty"); + String resourcePath = ClassUtils.convertClassNameToResourcePath(clazz.getName()) + suffix; + String prefixedResourcePath = ResourceUtils.CLASSPATH_URL_PREFIX + resourcePath; + ClassPathResource classPathResource = new ClassPathResource(resourcePath); + + if (classPathResource.exists()) { + if (logger.isInfoEnabled()) { + logger.info(String.format("Detected default resource location \"%s\" for test class [%s]", + prefixedResourcePath, clazz.getName())); + } + return new String[] { prefixedResourcePath }; } - return new String[] { prefixedResourcePath }; - } - else { - if (logger.isInfoEnabled()) { - logger.info(String.format("Could not detect default resource locations for test class [%s]: " + else if (logger.isDebugEnabled()) { + logger.debug(String.format("Did not detect default resource location for test class [%s]: " + "%s does not exist", clazz.getName(), classPathResource)); } - return EMPTY_STRING_ARRAY; } + + if (logger.isInfoEnabled()) { + logger.info(String.format("Could not detect default resource locations for test class [%s]: " + + "no resource found for suffixes %s.", clazz.getName(), ObjectUtils.nullSafeToString(suffixes))); + } + + return EMPTY_STRING_ARRAY; } /** @@ -250,13 +263,36 @@ public abstract class AbstractContextLoader implements SmartContextLoader { } /** - * Get the suffix to append to {@link ApplicationContext} resource locations - * when generating default locations. - *

Must be implemented by subclasses. - * @return the resource suffix; should not be {@code null} or empty + * Get the suffix to append to {@link ApplicationContext} resource + * locations when detecting default locations. + * + *

Subclasses must provide an implementation of this method that + * returns a single suffix. Alternatively subclasses may provide a + * no-op implementation of this method and override + * {@link #getResourceSuffixes()} in order to provide multiple custom + * suffixes. + * + * @return the resource suffix; never {@code null} or empty * @since 2.5 * @see #generateDefaultLocations(Class) + * @see #getResourceSuffixes() */ protected abstract String getResourceSuffix(); + /** + * Get the suffixes to append to {@link ApplicationContext} resource + * locations when detecting default locations. + * + *

The default implementation simply wraps the value returned by + * {@link #getResourceSuffix()} in a single-element array, but this + * can be overridden by subclasses in order to support multiple suffixes. + * + * @return the resource suffixes; never {@code null} or empty + * @since 4.1 + * @see #generateDefaultLocations(Class) + */ + protected String[] getResourceSuffixes() { + return new String[] { getResourceSuffix() }; + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java index 1f12c29325..17f6a04a71 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java @@ -35,26 +35,33 @@ import org.springframework.util.ObjectUtils; * {@code AbstractDelegatingSmartContextLoader} serves as an abstract base class * for implementations of the {@link SmartContextLoader} SPI that delegate to a * set of candidate SmartContextLoaders (i.e., one that supports XML - * configuration files and one that supports annotated classes) to determine which - * context loader is appropriate for a given test class's configuration. Each - * candidate is given a chance to {@link #processContextConfiguration process} the + * configuration files or Groovy scripts and one that supports annotated classes) + * to determine which context loader is appropriate for a given test class's + * configuration. Each candidate is given a chance to + * {@linkplain #processContextConfiguration process} the * {@link ContextConfigurationAttributes} for each class in the test class hierarchy * that is annotated with {@link ContextConfiguration @ContextConfiguration}, and * the candidate that supports the merged, processed configuration will be used to - * actually {@link #loadContext load} the context. + * actually {@linkplain #loadContext load} the context. + * + *

Any reference to an XML-based loader can be interpreted to mean + * a context loader that supports only XML configuration files or one that + * supports both XML configuration files and Groovy scripts simultaneously. * *

Placing an empty {@code @ContextConfiguration} annotation on a test class signals - * that default resource locations (i.e., XML configuration files) or default - * {@link org.springframework.context.annotation.Configuration configuration classes} + * that default resource locations (e.g., XML configuration files or Groovy scripts) + * or default + * {@linkplain org.springframework.context.annotation.Configuration configuration classes} * should be detected. Furthermore, if a specific {@link ContextLoader} or * {@link SmartContextLoader} is not explicitly declared via * {@code @ContextConfiguration}, a concrete subclass of * {@code AbstractDelegatingSmartContextLoader} will be used as the default loader, - * thus providing automatic support for either XML configuration files or annotated - * classes, but not both simultaneously. + * thus providing automatic support for either path-based resource locations + * (e.g., XML configuration files and Groovy scripts) or annotated classes, + * but not both simultaneously. * - *

As of Spring 3.2, a test class may optionally declare neither XML configuration - * files nor annotated classes and instead declare only {@linkplain + *

As of Spring 3.2, a test class may optionally declare neither path-based + * resource locations nor annotated classes and instead declare only {@linkplain * ContextConfiguration#initializers application context initializers}. In such * cases, an attempt will still be made to detect defaults, but their absence will * not result in an exception. @@ -69,7 +76,8 @@ public abstract class AbstractDelegatingSmartContextLoader implements SmartConte /** - * Get the delegate {@code SmartContextLoader} that supports XML configuration files. + * Get the delegate {@code SmartContextLoader} that supports XML configuration + * files and/or Groovy scripts. */ protected abstract SmartContextLoader getXmlLoader(); @@ -115,9 +123,9 @@ public abstract class AbstractDelegatingSmartContextLoader implements SmartConte * Delegates to candidate {@code SmartContextLoaders} to process the supplied * {@link ContextConfigurationAttributes}. *

Delegation is based on explicit knowledge of the implementations of the - * default loaders for {@link #getXmlLoader() XML configuration files} and - * {@link #getAnnotationConfigLoader() annotated classes}. Specifically, the - * delegation algorithm is as follows: + * default loaders for {@linkplain #getXmlLoader() XML configuration files and + * Groovy scripts} and {@linkplain #getAnnotationConfigLoader() annotated classes}. + * Specifically, the delegation algorithm is as follows: *