From a73280ccc8a18e0048351a3f08a51c2b13ec3b45 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 6 Oct 2012 22:24:55 +0200 Subject: [PATCH] Support loading WebApplicationContexts in the TCF Prior to this commit, the Spring TestContext Framework only supported loading an ApplicationContext in integration tests from either XML or Java Properties files (since Spring 2.5), and Spring 3.1 introduced support for loading an ApplicationContext in integration tests from annotated classes (e.g., @Configuration classes). All of the ContextLoader implementations used to provide this support load a GenericApplicationContext. However, a GenericApplicationContext is not suitable for testing a web application since a web application relies on an implementation of WebApplicationContext (WAC). This commit makes it possible to integration test Spring-powered web applications by adding the following functionality to the Spring TestContext Framework. - Introduced AbstractGenericWebContextLoader and two concrete subclasses: - XmlWebContextLoader - AnnotationConfigWebContextLoader - Pulled up prepareContext(context, mergedConfig) from AbstractGenericContextLoader into AbstractContextLoader to allow it to be shared across web and non-web context loaders. - Introduced AnnotationConfigContextLoaderUtils and refactored AnnotationConfigContextLoader accordingly. These utils are also used by AnnotationConfigWebContextLoader. - Introduced a new @WebAppConfiguration annotation to denote that the ApplicationContext loaded for a test should be a WAC and to configure the base resource path for the root directory of a web application. - Introduced WebMergedContextConfiguration which extends MergedContextConfiguration with support for a baseResourcePath for the root directory of a web application. - ContextLoaderUtils.buildMergedContextConfiguration() now builds a WebMergedContextConfiguration instead of a standard MergedContextConfiguration if @WebAppConfiguration is present on the test class. - Introduced a configureWebResources() method in AbstractGenericWebContextLoader that is responsible for creating a MockServletContext with a proper ResourceLoader for the resourceBasePath configured in the WebMergedContextConfiguration. The resulting mock ServletContext is set in the WAC, and the WAC is stored as the Root WAC in the ServletContext. - Introduced a WebTestExecutionListener that sets up default thread local state via RequestContextHolder before each test method by using the MockServletContext already present in the WAC and by creating a MockHttpServletRequest, MockHttpServletResponse, and ServletWebRequest that is set in the RequestContextHolder. WTEL also ensures that the MockHttpServletResponse and ServletWebRequest can be injected into the test instance (e.g., via @Autowired) and cleans up thread locals after each test method. - WebTestExecutionListener is configured as a default TestExecutionListener before DependencyInjectionTestExecutionListener - Extracted AbstractDelegatingSmartContextLoader from DelegatingSmartContextLoader and introduced a new WebDelegatingSmartContextLoader. - ContextLoaderUtils now selects the default delegating ContextLoader class name based on the presence of @WebAppConfiguration on the test class. - Tests in the spring-test-mvc module no longer use a custom ContextLoader to load a WebApplicationContext. Instead, they now rely on new core functionality provided in this commit. Issue: SPR-5243 --- .../context/GenericWebContextLoader.java | 94 ------ .../samples/context/JavaTestContextTests.java | 23 +- .../samples/context/SpringSecurityTests.java | 89 +++--- .../samples/context/XmlTestContextTests.java | 26 +- spring-test/.springBeans | 1 + .../test/context/ContextLoaderUtils.java | 18 +- .../context/MergedContextConfiguration.java | 2 +- .../test/context/TestContext.java | 4 +- .../test/context/TestContextManager.java | 54 ++-- .../support/AbstractContextLoader.java | 75 ++++- .../AbstractDelegatingSmartContextLoader.java | 287 ++++++++++++++++++ .../support/AbstractGenericContextLoader.java | 83 +---- .../AbstractGenericWebContextLoader.java | 126 ++++++++ .../AnnotationConfigContextLoader.java | 75 +---- .../AnnotationConfigContextLoaderUtils.java | 118 +++++++ .../AnnotationConfigWebContextLoader.java | 151 +++++++++ .../support/DelegatingSmartContextLoader.java | 249 +-------------- .../WebDelegatingSmartContextLoader.java | 47 +++ .../context/support/XmlWebContextLoader.java | 49 +++ .../test/context/web/WebAppConfiguration.java | 45 +++ .../web/WebMergedContextConfiguration.java | 122 ++++++++ .../context/web/WebTestExecutionListener.java | 122 ++++++++ .../context/TestExecutionListenersTests.java | 4 +- .../context/web/AbstractBasicWacTests.java | 92 ++++++ .../web/BasicAnnotationConfigWacTests.java | 48 +++ .../test/context/web/BasicXmlWacTests.java | 20 +- .../web/WebContextLoaderTestSuite.java | 40 +++ spring-test/src/test/resources/log4j.xml | 4 + .../context/web/BasicXmlWacTests-context.xml | 7 + 29 files changed, 1474 insertions(+), 601 deletions(-) delete mode 100644 spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/GenericWebContextLoader.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericWebContextLoader.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigWebContextLoader.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/support/WebDelegatingSmartContextLoader.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/support/XmlWebContextLoader.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/web/WebTestExecutionListener.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/web/AbstractBasicWacTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/web/BasicAnnotationConfigWacTests.java rename spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/WebContextLoader.java => spring-test/src/test/java/org/springframework/test/context/web/BasicXmlWacTests.java (63%) create mode 100644 spring-test/src/test/java/org/springframework/test/context/web/WebContextLoaderTestSuite.java create mode 100644 spring-test/src/test/resources/org/springframework/test/context/web/BasicXmlWacTests-context.xml diff --git a/spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/GenericWebContextLoader.java b/spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/GenericWebContextLoader.java deleted file mode 100644 index 55c477ccc342..000000000000 --- a/spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/GenericWebContextLoader.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2002-2012 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.web.mock.servlet.samples.context; - -import javax.servlet.RequestDispatcher; - -import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; -import org.springframework.context.annotation.AnnotationConfigUtils; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.core.io.FileSystemResourceLoader; -import org.springframework.core.io.ResourceLoader; -import org.springframework.mock.web.MockRequestDispatcher; -import org.springframework.mock.web.MockServletContext; -import org.springframework.test.context.MergedContextConfiguration; -import org.springframework.test.context.support.AbstractContextLoader; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.context.support.GenericWebApplicationContext; - -/** - * This class is here temporarily until the TestContext framework provides - * support for WebApplicationContext yet: - * - * https://jira.springsource.org/browse/SPR-5243 - * - *

After that this class will no longer be needed. It's provided here as an example - * and to serve as a temporary solution. - */ -public class GenericWebContextLoader extends AbstractContextLoader { - protected final MockServletContext servletContext; - - public GenericWebContextLoader(String warRootDir, boolean isClasspathRelative) { - ResourceLoader resourceLoader = isClasspathRelative ? new DefaultResourceLoader() : new FileSystemResourceLoader(); - this.servletContext = initServletContext(warRootDir, resourceLoader); - } - - private MockServletContext initServletContext(String warRootDir, ResourceLoader resourceLoader) { - return new MockServletContext(warRootDir, resourceLoader) { - // Required for DefaultServletHttpRequestHandler... - public RequestDispatcher getNamedDispatcher(String path) { - return (path.equals("default")) ? new MockRequestDispatcher(path) : super.getNamedDispatcher(path); - } - }; - } - - public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception { - GenericWebApplicationContext context = new GenericWebApplicationContext(); - context.getEnvironment().setActiveProfiles(mergedConfig.getActiveProfiles()); - prepareContext(context); - loadBeanDefinitions(context, mergedConfig); - return context; - } - - public ApplicationContext loadContext(String... locations) throws Exception { - // should never be called - throw new UnsupportedOperationException(); - } - - protected void prepareContext(GenericWebApplicationContext context) { - this.servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, context); - context.setServletContext(this.servletContext); - } - - protected void loadBeanDefinitions(GenericWebApplicationContext context, String[] locations) { - new XmlBeanDefinitionReader(context).loadBeanDefinitions(locations); - AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - context.refresh(); - context.registerShutdownHook(); - } - - protected void loadBeanDefinitions(GenericWebApplicationContext context, MergedContextConfiguration mergedConfig) { - new AnnotatedBeanDefinitionReader(context).register(mergedConfig.getClasses()); - loadBeanDefinitions(context, mergedConfig.getLocations()); - } - - @Override - protected String getResourceSuffix() { - return "-context.xml"; - } -} diff --git a/spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/JavaTestContextTests.java b/spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/JavaTestContextTests.java index 722acba41e71..dbb5ff73aebd 100644 --- a/spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/JavaTestContextTests.java +++ b/spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/JavaTestContextTests.java @@ -16,9 +16,8 @@ package org.springframework.test.web.mock.servlet.samples.context; -import static org.springframework.test.web.mock.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.mock.servlet.result.MockMvcResultMatchers.forwardedUrl; -import static org.springframework.test.web.mock.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.mock.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.mock.servlet.result.MockMvcResultMatchers.*; import org.junit.Before; import org.junit.Test; @@ -26,21 +25,20 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.mock.servlet.MockMvc; import org.springframework.test.web.mock.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; /** * Tests with Java configuration. * - * The TestContext framework doesn't support WebApplicationContext yet: - * https://jira.springsource.org/browse/SPR-5243 - * - * A custom {@link ContextLoader} is used to load the WebApplicationContext. + * @author Rossen Stoyanchev + * @author Sam Brannen */ @RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(loader=WebContextLoader.class, classes={WebConfig.class}) +@WebAppConfiguration("src/test/resources/META-INF/web-resources") +@ContextConfiguration(classes = WebConfig.class) public class JavaTestContextTests { @Autowired @@ -48,6 +46,7 @@ public class JavaTestContextTests { private MockMvc mockMvc; + @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); @@ -55,9 +54,9 @@ public void setup() { @Test public void tilesDefinitions() throws Exception { - this.mockMvc.perform(get("/")) - .andExpect(status().isOk()) - .andExpect(forwardedUrl("/WEB-INF/layouts/standardLayout.jsp")); + this.mockMvc.perform(get("/"))// + .andExpect(status().isOk())// + .andExpect(forwardedUrl("/WEB-INF/layouts/standardLayout.jsp")); } } diff --git a/spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/SpringSecurityTests.java b/spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/SpringSecurityTests.java index e24bdba57da6..b1da11b99ffd 100644 --- a/spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/SpringSecurityTests.java +++ b/spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/SpringSecurityTests.java @@ -10,6 +10,7 @@ * 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.web.mock.servlet.samples.context; import static org.springframework.test.web.mock.servlet.request.MockMvcRequestBuilders.get; @@ -34,6 +35,7 @@ import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.mock.servlet.MockMvc; import org.springframework.test.web.mock.servlet.MvcResult; import org.springframework.test.web.mock.servlet.ResultMatcher; @@ -44,32 +46,25 @@ /** * Basic example that includes Spring Security configuration. * - *

Note that currently there are no {@link ResultMatcher}' built specifically - * for asserting the Spring Security context. However, it's quite easy to put - * them together as shown below and Spring Security extensions will become - * available in the near future. + *

Note that currently there are no {@linkplain ResultMatcher ResultMatchers} + * built specifically for asserting the Spring Security context. However, it's + * quite easy to put them together as shown below, and Spring Security extensions + * will become available in the near future. * *

This also demonstrates a custom {@link RequestPostProcessor} which authenticates * a user to a particular {@link HttpServletRequest}. * - *

Also see the Javadoc of {@link GenericWebContextLoader}, a class that - * provides temporary support for loading WebApplicationContext by extending - * the TestContext framework. - * * @author Rob Winch * @author Rossen Stoyanchev + * @author Sam Brannen * @see SecurityRequestPostProcessors */ @RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration( - loader=WebContextLoader.class, - value={ - "classpath:org/springframework/test/web/mock/servlet/samples/context/security.xml", - "classpath:org/springframework/test/web/mock/servlet/samples/servlet-context.xml" - }) +@WebAppConfiguration("src/test/resources/META-INF/web-resources") +@ContextConfiguration({ "security.xml", "../servlet-context.xml" }) public class SpringSecurityTests { - private static String SEC_CONTEXT_ATTR = HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; + private static final String SEC_CONTEXT_ATTR = HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; @Autowired private FilterChainProxy springSecurityFilterChain; @@ -79,57 +74,67 @@ public class SpringSecurityTests { private MockMvc mockMvc; + @Before public void setup() { - this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac) - .addFilters(this.springSecurityFilterChain).build(); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)// + .addFilters(this.springSecurityFilterChain)// + .build(); } @Test public void requiresAuthentication() throws Exception { - mockMvc.perform(get("/user")) - .andExpect(redirectedUrl("http://localhost/spring_security_login")); + mockMvc.perform(get("/user")).// + andExpect(redirectedUrl("http://localhost/spring_security_login")); } @Test public void accessGranted() throws Exception { - this.mockMvc.perform(get("/").with(userDeatilsService("user"))) - .andExpect(status().isOk()) - .andExpect(forwardedUrl("/WEB-INF/layouts/standardLayout.jsp")); + this.mockMvc.perform(get("/").// + with(userDeatilsService("user"))).// + andExpect(status().isOk()).// + andExpect(forwardedUrl("/WEB-INF/layouts/standardLayout.jsp")); } @Test public void accessDenied() throws Exception { - this.mockMvc.perform(get("/").with(user("user").roles("DENIED"))) - .andExpect(status().isForbidden()); + this.mockMvc.perform(get("/")// + .with(user("user").roles("DENIED")))// + .andExpect(status().isForbidden()); } @Test public void userAuthenticates() throws Exception { final String username = "user"; - mockMvc.perform(post("/j_spring_security_check").param("j_username", username).param("j_password", "password")) - .andExpect(redirectedUrl("/")) - .andExpect(new ResultMatcher() { - public void match(MvcResult mvcResult) throws Exception { - HttpSession session = mvcResult.getRequest().getSession(); - SecurityContext securityContext = (SecurityContext) session.getAttribute(SEC_CONTEXT_ATTR); - Assert.assertEquals(securityContext.getAuthentication().getName(), username); - } - }); + mockMvc.perform(post("/j_spring_security_check").// + param("j_username", username).// + param("j_password", "password")).// + andExpect(redirectedUrl("/")).// + andExpect(new ResultMatcher() { + + public void match(MvcResult mvcResult) throws Exception { + HttpSession session = mvcResult.getRequest().getSession(); + SecurityContext securityContext = (SecurityContext) session.getAttribute(SEC_CONTEXT_ATTR); + Assert.assertEquals(securityContext.getAuthentication().getName(), username); + } + }); } @Test public void userAuthenticateFails() throws Exception { final String username = "user"; - mockMvc.perform(post("/j_spring_security_check").param("j_username", username).param("j_password", "invalid")) - .andExpect(redirectedUrl("/spring_security_login?login_error")) - .andExpect(new ResultMatcher() { - public void match(MvcResult mvcResult) throws Exception { - HttpSession session = mvcResult.getRequest().getSession(); - SecurityContext securityContext = (SecurityContext) session.getAttribute(SEC_CONTEXT_ATTR); - Assert.assertNull(securityContext); - } - }); + mockMvc.perform(post("/j_spring_security_check").// + param("j_username", username).// + param("j_password", "invalid")).// + andExpect(redirectedUrl("/spring_security_login?login_error")).// + andExpect(new ResultMatcher() { + + public void match(MvcResult mvcResult) throws Exception { + HttpSession session = mvcResult.getRequest().getSession(); + SecurityContext securityContext = (SecurityContext) session.getAttribute(SEC_CONTEXT_ATTR); + Assert.assertNull(securityContext); + } + }); } } diff --git a/spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/XmlTestContextTests.java b/spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/XmlTestContextTests.java index 2edc313d033d..3274c6137d4d 100644 --- a/spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/XmlTestContextTests.java +++ b/spring-test-mvc/src/test/java/org/springframework/test/web/mock/servlet/samples/context/XmlTestContextTests.java @@ -16,9 +16,8 @@ package org.springframework.test.web.mock.servlet.samples.context; -import static org.springframework.test.web.mock.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.mock.servlet.result.MockMvcResultMatchers.forwardedUrl; -import static org.springframework.test.web.mock.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.mock.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.mock.servlet.result.MockMvcResultMatchers.*; import org.junit.Before; import org.junit.Test; @@ -26,23 +25,20 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.mock.servlet.MockMvc; import org.springframework.test.web.mock.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; /** * Tests with XML configuration. * - * The TestContext framework doesn't support WebApplicationContext yet: - * https://jira.springsource.org/browse/SPR-5243 - * - * A custom {@link ContextLoader} is used to load the WebApplicationContext. + * @author Rossen Stoyanchev + * @author Sam Brannen */ @RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration( - loader=WebContextLoader.class, - locations={"/org/springframework/test/web/mock/servlet/samples/servlet-context.xml"}) +@WebAppConfiguration("src/test/resources/META-INF/web-resources") +@ContextConfiguration("../servlet-context.xml") public class XmlTestContextTests { @Autowired @@ -50,6 +46,7 @@ public class XmlTestContextTests { private MockMvc mockMvc; + @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); @@ -57,10 +54,9 @@ public void setup() { @Test public void tilesDefinitions() throws Exception { - this.mockMvc.perform(get("/")) - .andExpect(status().isOk()) - .andExpect(forwardedUrl("/WEB-INF/layouts/standardLayout.jsp")); + this.mockMvc.perform(get("/"))// + .andExpect(status().isOk())// + .andExpect(forwardedUrl("/WEB-INF/layouts/standardLayout.jsp")); } } - diff --git a/spring-test/.springBeans b/spring-test/.springBeans index 7d64f21cfb8a..a4a7e241718f 100644 --- a/spring-test/.springBeans +++ b/spring-test/.springBeans @@ -9,6 +9,7 @@ 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 + src/test/resources/org/springframework/test/context/web/BasicXmlWacTests-config.xml 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 a5ad3516f056..781f7bd7a2c1 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 @@ -16,8 +16,8 @@ package org.springframework.test.context; -import static org.springframework.beans.BeanUtils.instantiateClass; -import static org.springframework.core.annotation.AnnotationUtils.findAnnotationDeclaringClass; +import static org.springframework.beans.BeanUtils.*; +import static org.springframework.core.annotation.AnnotationUtils.*; import java.util.ArrayList; import java.util.Arrays; @@ -30,6 +30,8 @@ import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.web.WebMergedContextConfiguration; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -54,6 +56,7 @@ abstract class ContextLoaderUtils { 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.support.WebDelegatingSmartContextLoader"; private ContextLoaderUtils() { @@ -83,7 +86,8 @@ static ContextLoader resolveContextLoader(Class testClass, String defaultCont Assert.notNull(testClass, "Test class must not be null"); if (!StringUtils.hasText(defaultContextLoaderClassName)) { - defaultContextLoaderClassName = DEFAULT_CONTEXT_LOADER_CLASS_NAME; + defaultContextLoaderClassName = testClass.isAnnotationPresent(WebAppConfiguration.class) ? DEFAULT_WEB_CONTEXT_LOADER_CLASS_NAME + : DEFAULT_CONTEXT_LOADER_CLASS_NAME; } Class contextLoaderClass = resolveContextLoaderClass(testClass, @@ -394,6 +398,14 @@ static MergedContextConfiguration buildMergedContextConfiguration(Class testC Set>> initializerClasses = resolveInitializerClasses(configAttributesList); String[] activeProfiles = resolveActiveProfiles(testClass); + if (testClass.isAnnotationPresent(WebAppConfiguration.class)) { + WebAppConfiguration webAppConfig = testClass.getAnnotation(WebAppConfiguration.class); + String resourceBasePath = webAppConfig.value(); + return new WebMergedContextConfiguration(testClass, locations, classes, initializerClasses, activeProfiles, + resourceBasePath, contextLoader); + } + + // else 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 e484253b8c37..6fc402e92ffb 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 @@ -105,7 +105,7 @@ private static String[] processActiveProfiles(String[] activeProfiles) { * {@link ContextLoader} based solely on the fully qualified name of the * loader or "null" if the supplied loaded is null. */ - private static String nullSafeToString(ContextLoader contextLoader) { + protected static String nullSafeToString(ContextLoader contextLoader) { return contextLoader == null ? "null" : contextLoader.getClass().getName(); } diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContext.java b/spring-test/src/main/java/org/springframework/test/context/TestContext.java index 7b7426b1049f..6d3a90c663d0 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContext.java @@ -114,7 +114,7 @@ public class TestContext extends AttributeAccessorSupport { */ private ApplicationContext loadApplicationContext() throws Exception { ContextLoader contextLoader = mergedContextConfiguration.getContextLoader(); - Assert.notNull(contextLoader, "Can not load an ApplicationContext with a NULL 'contextLoader'. " + Assert.notNull(contextLoader, "Cannot load an ApplicationContext with a NULL 'contextLoader'. " + "Consider annotating your test class with @ContextConfiguration."); ApplicationContext applicationContext; @@ -125,7 +125,7 @@ private ApplicationContext loadApplicationContext() throws Exception { } else { String[] locations = mergedContextConfiguration.getLocations(); - Assert.notNull(locations, "Can not load an ApplicationContext with a NULL 'locations' array. " + Assert.notNull(locations, "Cannot load an ApplicationContext with a NULL 'locations' array. " + "Consider annotating your test class with @ContextConfiguration."); applicationContext = contextLoader.loadContext(locations); } 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 b5ba968ba0ca..e3e42b6706c9 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 @@ -76,6 +76,7 @@ public class TestContextManager { private static final String[] DEFAULT_TEST_EXECUTION_LISTENER_CLASS_NAMES = new String[] { + "org.springframework.test.context.web.WebTestExecutionListener", "org.springframework.test.context.support.DependencyInjectionTestExecutionListener", "org.springframework.test.context.support.DirtiesContextTestExecutionListener", "org.springframework.test.context.transaction.TransactionalTestExecutionListener" }; @@ -126,7 +127,6 @@ protected final TestContext getTestContext() { return this.testContext; } - /** * Register the supplied {@link TestExecutionListener TestExecutionListeners} * by appending them to the set of listeners used by this TestContextManager. @@ -155,8 +155,8 @@ public final List getTestExecutionListeners() { * registered for this TestContextManager in reverse order. */ private List getReversedTestExecutionListeners() { - List listenersReversed = - new ArrayList(getTestExecutionListeners()); + List listenersReversed = new ArrayList( + getTestExecutionListeners()); Collections.reverse(listenersReversed); return listenersReversed; } @@ -186,8 +186,7 @@ private TestExecutionListener[] retrieveTestExecutionListeners(Class clazz) { } classesList.addAll(getDefaultTestExecutionListenerClasses()); defaultListeners = true; - } - else { + } else { // Traverse the class hierarchy... while (declaringClass != null) { TestExecutionListeners testExecutionListeners = declaringClass.getAnnotation(annotationType); @@ -200,22 +199,21 @@ private TestExecutionListener[] retrieveTestExecutionListeners(Class clazz) { Class[] listenerClasses = testExecutionListeners.listeners(); if (!ObjectUtils.isEmpty(valueListenerClasses) && !ObjectUtils.isEmpty(listenerClasses)) { String msg = String.format( - "Test class [%s] has been configured with @TestExecutionListeners' 'value' [%s] " + - "and 'listeners' [%s] attributes. Use one or the other, but not both.", + "Test 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)) { + } else if (!ObjectUtils.isEmpty(valueListenerClasses)) { listenerClasses = valueListenerClasses; } if (listenerClasses != null) { classesList.addAll(0, Arrays.> asList(listenerClasses)); } - declaringClass = (testExecutionListeners.inheritListeners() ? - AnnotationUtils.findAnnotationDeclaringClass(annotationType, declaringClass.getSuperclass()) : null); + declaringClass = (testExecutionListeners.inheritListeners() ? AnnotationUtils.findAnnotationDeclaringClass( + annotationType, declaringClass.getSuperclass()) : null); } } @@ -223,16 +221,14 @@ else if (!ObjectUtils.isEmpty(valueListenerClasses)) { for (Class listenerClass : classesList) { try { listeners.add(BeanUtils.instantiateClass(listenerClass)); - } - catch (NoClassDefFoundError err) { + } catch (NoClassDefFoundError err) { if (defaultListeners) { if (logger.isDebugEnabled()) { logger.debug("Could not instantiate default TestExecutionListener class [" + listenerClass.getName() + "]. Specify custom listener classes or make the default listener classes available."); } - } - else { + } else { throw err; } } @@ -245,24 +241,21 @@ else if (!ObjectUtils.isEmpty(valueListenerClasses)) { */ @SuppressWarnings("unchecked") protected Set> getDefaultTestExecutionListenerClasses() { - Set> defaultListenerClasses = - new LinkedHashSet>(); + Set> defaultListenerClasses = new LinkedHashSet>(); for (String className : DEFAULT_TEST_EXECUTION_LISTENER_CLASS_NAMES) { try { - defaultListenerClasses.add( - (Class) getClass().getClassLoader().loadClass(className)); - } - catch (Throwable ex) { + 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."); + + "]. 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 @@ -286,8 +279,7 @@ public void beforeTestClass() throws Exception { for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) { try { testExecutionListener.beforeTestClass(getTestContext()); - } - catch (Exception ex) { + } catch (Exception ex) { logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + "] to process 'before class' callback for test class [" + testClass + "]", ex); throw ex; @@ -319,8 +311,7 @@ public void prepareTestInstance(Object testInstance) throws Exception { for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) { try { testExecutionListener.prepareTestInstance(getTestContext()); - } - catch (Exception ex) { + } catch (Exception ex) { logger.error("Caught exception while allowing TestExecutionListener [" + testExecutionListener + "] to prepare test instance [" + testInstance + "]", ex); throw ex; @@ -356,8 +347,7 @@ public void beforeTestMethod(Object testInstance, Method testMethod) throws Exce for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) { try { testExecutionListener.beforeTestMethod(getTestContext()); - } - catch (Exception ex) { + } catch (Exception ex) { logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + "] to process 'before' execution of test method [" + testMethod + "] for test instance [" + testInstance + "]", ex); @@ -404,8 +394,7 @@ public void afterTestMethod(Object testInstance, Method testMethod, Throwable ex for (TestExecutionListener testExecutionListener : getReversedTestExecutionListeners()) { try { testExecutionListener.afterTestMethod(getTestContext()); - } - catch (Exception ex) { + } catch (Exception ex) { logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + "] to process 'after' execution for test: method [" + testMethod + "], instance [" + testInstance + "], exception [" + exception + "]", ex); @@ -446,8 +435,7 @@ public void afterTestClass() throws Exception { for (TestExecutionListener testExecutionListener : getReversedTestExecutionListeners()) { try { testExecutionListener.afterTestClass(getTestContext()); - } - catch (Exception ex) { + } catch (Exception ex) { logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + "] to process 'after class' callback for test class [" + testClass + "]", ex); if (afterTestClassException == null) { 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 0fe803569501..6f114738fff8 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 @@ -16,13 +16,24 @@ 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.context.ApplicationContext; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.ResourcePatternUtils; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.SmartContextLoader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -81,6 +92,64 @@ public void processContextConfiguration(ContextConfigurationAttributes configAtt configAttributes.setLocations(processedLocations); } + /** + * Prepare the {@link ConfigurableApplicationContext} created by this + * {@code SmartContextLoader} before bean definitions are read. + * + *

The default implementation: + *

+ * + *

Any {@code ApplicationContextInitializers} implementing + * {@link org.springframework.core.Ordered Ordered} or marked with {@link + * org.springframework.core.annotation.Order @Order} will be sorted appropriately. + * + * @param context the newly created application context + * @param mergedConfig the merged context configuration + * @see ApplicationContextInitializer#initialize(ConfigurableApplicationContext) + * @see #loadContext(MergedContextConfiguration) + * @see ConfigurableApplicationContext#setId + * @since 3.2 + */ + @SuppressWarnings("unchecked") + protected void prepareContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { + + context.getEnvironment().setActiveProfiles(mergedConfig.getActiveProfiles()); + + Set>> initializerClasses = mergedConfig.getContextInitializerClasses(); + + if (initializerClasses.size() == 0) { + // no ApplicationContextInitializers have been declared -> nothing to do + return; + } + + final List> initializerInstances = new ArrayList>(); + final Class contextClass = context.getClass(); + + for (Class> initializerClass : initializerClasses) { + Class initializerContextClass = GenericTypeResolver.resolveTypeArgument(initializerClass, + ApplicationContextInitializer.class); + Assert.isAssignable(initializerContextClass, contextClass, String.format( + "Could not add context initializer [%s] since its generic parameter [%s] " + + "is not assignable from the type of application context used by this " + + "context loader [%s]: ", initializerClass.getName(), initializerContextClass.getName(), + contextClass.getName())); + initializerInstances.add((ApplicationContextInitializer) BeanUtils.instantiateClass(initializerClass)); + } + + Collections.sort(initializerInstances, new AnnotationAwareOrderComparator()); + for (ApplicationContextInitializer initializer : initializerInstances) { + initializer.initialize(context); + } + } + // --- ContextLoader ------------------------------------------------------- /** @@ -185,12 +254,10 @@ protected String[] modifyLocations(Class clazz, String... locations) { String path = locations[i]; if (path.startsWith(SLASH)) { modifiedLocations[i] = ResourceUtils.CLASSPATH_URL_PREFIX + path; - } - else if (!ResourcePatternUtils.isUrl(path)) { + } else if (!ResourcePatternUtils.isUrl(path)) { modifiedLocations[i] = ResourceUtils.CLASSPATH_URL_PREFIX + SLASH + StringUtils.cleanPath(ClassUtils.classPackageAsResourcePath(clazz) + SLASH + path); - } - else { + } else { modifiedLocations[i] = StringUtils.cleanPath(path); } } 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 new file mode 100644 index 000000000000..5a05a24c4ed1 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java @@ -0,0 +1,287 @@ +/* + * Copyright 2002-2012 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.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContext; +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.SmartContextLoader; +import org.springframework.util.Assert; +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 + * {@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. + * + *

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

As of Spring 3.2, a test class may optionally declare neither XML configuration + * files 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 an an exception. + * + * @author Sam Brannen + * @since 3.2 + * @see SmartContextLoader + */ +abstract class AbstractDelegatingSmartContextLoader implements SmartContextLoader { + + private static final Log logger = LogFactory.getLog(AbstractDelegatingSmartContextLoader.class); + + + /** + * Get the delegate {@code SmartContextLoader} that supports XML configuration files. + */ + protected abstract SmartContextLoader getXmlLoader(); + + /** + * Get the delegate {@code SmartContextLoader} that supports annotated classes. + */ + protected abstract SmartContextLoader getAnnotationConfigLoader(); + + // --- SmartContextLoader -------------------------------------------------- + + private static String name(SmartContextLoader loader) { + return loader.getClass().getSimpleName(); + } + + private static void delegateProcessing(SmartContextLoader loader, ContextConfigurationAttributes configAttributes) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Delegating to %s to process context configuration %s.", name(loader), + configAttributes)); + } + loader.processContextConfiguration(configAttributes); + } + + private static ApplicationContext delegateLoading(SmartContextLoader loader, MergedContextConfiguration mergedConfig) + throws Exception { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Delegating to %s to load context from %s.", name(loader), mergedConfig)); + } + return loader.loadContext(mergedConfig); + } + + private boolean supports(SmartContextLoader loader, MergedContextConfiguration mergedConfig) { + if (loader == getAnnotationConfigLoader()) { + return ObjectUtils.isEmpty(mergedConfig.getLocations()) && !ObjectUtils.isEmpty(mergedConfig.getClasses()); + } else { + return !ObjectUtils.isEmpty(mergedConfig.getLocations()) && ObjectUtils.isEmpty(mergedConfig.getClasses()); + } + } + + /** + * 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: + * + *

+ * + * @param configAttributes the context configuration attributes to process + * @throws IllegalArgumentException if the supplied configuration attributes are + * null, or if the supplied configuration attributes include both + * resource locations and annotated classes + * @throws IllegalStateException if the XML-based loader detects default + * configuration classes; if the annotation-based loader detects default + * resource locations; if neither candidate loader detects defaults for the supplied + * context configuration; or if both candidate loaders detect defaults for the + * supplied context configuration + */ + public void processContextConfiguration(final ContextConfigurationAttributes configAttributes) { + + Assert.notNull(configAttributes, "configAttributes must not be null"); + Assert.isTrue(!(configAttributes.hasLocations() && configAttributes.hasClasses()), String.format( + "Cannot process locations AND classes for context " + + "configuration %s; configure one or the other, but not both.", configAttributes)); + + // If the original locations or classes were not empty, there's no + // need to bother with default detection checks; just let the + // appropriate loader process the configuration. + if (configAttributes.hasLocations()) { + delegateProcessing(getXmlLoader(), configAttributes); + } else if (configAttributes.hasClasses()) { + delegateProcessing(getAnnotationConfigLoader(), configAttributes); + } else { + // Else attempt to detect defaults... + + // Let the XML loader process the configuration. + delegateProcessing(getXmlLoader(), configAttributes); + boolean xmlLoaderDetectedDefaults = configAttributes.hasLocations(); + + if (xmlLoaderDetectedDefaults) { + if (logger.isInfoEnabled()) { + logger.info(String.format("%s detected default locations for context configuration %s.", + name(getXmlLoader()), configAttributes)); + } + } + + if (configAttributes.hasClasses()) { + throw new IllegalStateException(String.format( + "%s should NOT have detected default configuration classes for context configuration %s.", + name(getXmlLoader()), configAttributes)); + } + + // Now let the annotation config loader process the configuration. + delegateProcessing(getAnnotationConfigLoader(), configAttributes); + + if (configAttributes.hasClasses()) { + if (logger.isInfoEnabled()) { + logger.info(String.format( + "%s detected default configuration classes for context configuration %s.", + name(getAnnotationConfigLoader()), configAttributes)); + } + } + + if (!xmlLoaderDetectedDefaults && configAttributes.hasLocations()) { + throw new IllegalStateException(String.format( + "%s should NOT have detected default locations for context configuration %s.", + name(getAnnotationConfigLoader()), configAttributes)); + } + + // If neither loader detected defaults and no initializers were declared, + // throw an exception. + if (!configAttributes.hasResources() && ObjectUtils.isEmpty(configAttributes.getInitializers())) { + throw new IllegalStateException(String.format( + "Neither %s nor %s was able to detect defaults, and no ApplicationContextInitializers " + + "were declared for context configuration %s", name(getXmlLoader()), + name(getAnnotationConfigLoader()), configAttributes)); + } + + if (configAttributes.hasLocations() && configAttributes.hasClasses()) { + String message = String.format( + "Configuration error: both default locations AND default configuration classes " + + "were detected for context configuration %s; configure one or the other, but not both.", + configAttributes); + logger.error(message); + throw new IllegalStateException(message); + } + } + } + + /** + * Delegates to an appropriate candidate {@code SmartContextLoader} to load + * an {@link ApplicationContext}. + * + *

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

+ * + * @param mergedConfig the merged context configuration to use to load the application context + * @throws IllegalArgumentException if the supplied merged configuration is null + * @throws IllegalStateException if neither candidate loader is capable of loading an + * {@code ApplicationContext} from the supplied merged context configuration + */ + public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception { + Assert.notNull(mergedConfig, "mergedConfig must not be null"); + + List candidates = Arrays.asList(getXmlLoader(), getAnnotationConfigLoader()); + + for (SmartContextLoader loader : candidates) { + // Determine if each loader can load a context from the mergedConfig. If it + // can, let it; otherwise, keep iterating. + if (supports(loader, mergedConfig)) { + return delegateLoading(loader, mergedConfig); + } + } + + // If neither of the candidates supports the mergedConfig based on resources but + // ACIs were declared, then delegate to the annotation config loader. + if (!mergedConfig.getContextInitializerClasses().isEmpty()) { + return delegateLoading(getAnnotationConfigLoader(), mergedConfig); + } + + throw new IllegalStateException(String.format( + "Neither %s nor %s was able to load an ApplicationContext from %s.", name(getXmlLoader()), + name(getAnnotationConfigLoader()), mergedConfig)); + } + + // --- ContextLoader ------------------------------------------------------- + + /** + * {@code AbstractDelegatingSmartContextLoader} does not support the + * {@link ContextLoader#processLocations(Class, String...)} method. Call + * {@link #processContextConfiguration(ContextConfigurationAttributes)} instead. + * @throws UnsupportedOperationException + */ + public final String[] processLocations(Class clazz, String... locations) { + throw new UnsupportedOperationException("DelegatingSmartContextLoaders do not support the ContextLoader SPI. " + + "Call processContextConfiguration(ContextConfigurationAttributes) instead."); + } + + /** + * {@code AbstractDelegatingSmartContextLoader} does not support the + * {@link ContextLoader#loadContext(String...) } method. Call + * {@link #loadContext(MergedContextConfiguration)} instead. + * @throws UnsupportedOperationException + */ + public final ApplicationContext loadContext(String... locations) throws Exception { + throw new UnsupportedOperationException("DelegatingSmartContextLoaders do not support the ContextLoader SPI. " + + "Call loadContext(MergedContextConfiguration) instead."); + } + +} 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 06145c6e94b0..9dba80f96901 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,25 +16,14 @@ 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; /** @@ -77,7 +66,10 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader * *