diff --git a/spring-test/src/main/java/org/springframework/test/context/web/ServletTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/web/ServletTestExecutionListener.java index f662752c970..a1bdb5a2ebf 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/ServletTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/ServletTestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import org.springframework.test.context.support.AbstractTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.util.Assert; import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletWebRequest; @@ -85,6 +86,18 @@ public class ServletTestExecutionListener extends AbstractTestExecutionListener public static final String POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE = Conventions.getQualifiedAttributeName( ServletTestExecutionListener.class, "populatedRequestContextHolder"); + /** + * Attribute name for a request attribute which indicates that the + * {@link MockHttpServletRequest} stored in the {@link RequestAttributes} + * in Spring Web's {@link RequestContextHolder} was created by the TestContext + * framework. + * + *

Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}. + * @since 4.2 + */ + public static final String CREATED_BY_THE_TESTCONTEXT_FRAMEWORK = Conventions.getQualifiedAttributeName( + ServletTestExecutionListener.class, "createdByTheTestContextFramework"); + private static final Log logger = LogFactory.getLog(ServletTestExecutionListener.class); @@ -184,6 +197,7 @@ public class ServletTestExecutionListener extends AbstractTestExecutionListener MockServletContext mockServletContext = (MockServletContext) servletContext; MockHttpServletRequest request = new MockHttpServletRequest(mockServletContext); + request.setAttribute(CREATED_BY_THE_TESTCONTEXT_FRAMEWORK, Boolean.TRUE); MockHttpServletResponse response = new MockHttpServletResponse(); ServletWebRequest servletWebRequest = new ServletWebRequest(request, response); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvc.java b/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvc.java index 91ee692adab..8ebabae2198 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvc.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvc.java @@ -26,6 +26,7 @@ import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.util.Assert; +import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; @@ -145,6 +146,7 @@ public final class MockMvc { // [SPR-13217] Simulate RequestContextFilter to ensure that RequestAttributes are // populated before filters are invoked. + RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes(); RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response)); MockFilterChain filterChain = new MockFilterChain(this.servlet, this.filters); @@ -152,6 +154,8 @@ public final class MockMvc { applyDefaultResultActions(mvcResult); + RequestContextHolder.setRequestAttributes(previousAttributes); + return new ResultActions() { @Override diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java index 7d8785b75de..41c315ada00 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import java.util.Map.Entry; import javax.servlet.ServletContext; import javax.servlet.ServletRequest; import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; import org.springframework.beans.Mergeable; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -38,6 +39,7 @@ import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.context.web.ServletTestExecutionListener; import org.springframework.test.web.servlet.MockMvc; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; @@ -46,6 +48,9 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.ValueConstants; import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.FlashMap; @@ -64,6 +69,7 @@ import org.springframework.web.util.UriUtils; * * @author Rossen Stoyanchev * @author Arjen Poutsma + * @author Sam Brannen * @since 3.2 */ public class MockHttpServletRequestBuilder @@ -639,10 +645,31 @@ public class MockHttpServletRequestBuilder } /** - * Create a new {@link MockHttpServletRequest} based on the given - * {@link ServletContext}. Can be overridden in subclasses. + * Create a {@link MockHttpServletRequest}. + *

If an instance of {@code MockHttpServletRequest} that was created + * by the Spring TestContext Framework is available via the + * {@link RequestAttributes} bound to the current thread in + * {@link RequestContextHolder}, this method simply returns that instance. + *

Otherwise, this method creates a new {@code MockHttpServletRequest} + * based on the supplied {@link ServletContext}. + *

Can be overridden in subclasses. + * @see RequestContextHolder#getRequestAttributes() + * @see ServletRequestAttributes + * @see ServletTestExecutionListener#CREATED_BY_THE_TESTCONTEXT_FRAMEWORK */ protected MockHttpServletRequest createServletRequest(ServletContext servletContext) { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (requestAttributes instanceof ServletRequestAttributes) { + HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); + if (request instanceof MockHttpServletRequest) { + MockHttpServletRequest mockRequest = (MockHttpServletRequest) request; + Object createdByTcf = mockRequest.getAttribute(ServletTestExecutionListener.CREATED_BY_THE_TESTCONTEXT_FRAMEWORK); + if (Boolean.TRUE.equals(createdByTcf)) { + return mockRequest; + } + } + } + return new MockHttpServletRequest(servletContext); } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java index 9026c9f9911..f9f77d36d67 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -105,6 +105,12 @@ public class MockMultipartHttpServletRequestBuilder extends MockHttpServletReque return this; } + /** + * Create a new {@link MockMultipartHttpServletRequest} based on the + * supplied {@code ServletContext} and the {@code MockMultipartFiles} + * added to this builder. + *

Can be overridden in subclasses. + */ @Override protected final MockHttpServletRequest createServletRequest(ServletContext servletContext) { MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(servletContext); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java index 3a707ac2e04..85b2c98b847 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.test.web.servlet.request; import java.net.URI; @@ -20,19 +21,31 @@ import javax.servlet.ServletContext; import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockMultipartHttpServletRequest; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.RequestBuilder; /** - * Static factory methods for {@link RequestBuilder}s. + * Static factory methods for {@link RequestBuilder RequestBuilders}. * - *

Eclipse users: Consider adding this class as a Java - * editor favorite. To navigate, open the Preferences and type "favorites". + *

Integration with the Spring TestContext Framework

+ *

Methods in this class will reuse a + * {@link org.springframework.mock.web.MockServletContext MockServletContext} + * that was created by the Spring TestContext Framework. + * + *

Methods in this class that return a {@link MockHttpServletRequestBuilder} + * will reuse a {@link MockHttpServletRequest} that was created by the Spring + * TestContext Framework. + * + *

Eclipse Users

+ *

Consider adding this class as a Java editor favorite. To navigate to + * this setting, open the Preferences and type "favorites". * * @author Arjen Poutsma * @author Rossen Stoyanchev * @author Greg Turnquist * @author Sebastien Deleuze + * @author Sam Brannen * @since 3.2 */ public abstract class MockMvcRequestBuilders { @@ -185,7 +198,12 @@ public abstract class MockMvcRequestBuilders { } /** - * Create a {@link MockHttpServletRequestBuilder} for a multipart request. + * Create a {@link MockMultipartHttpServletRequestBuilder} for a multipart request. + *

In contrast to methods in this class that return a + * {@link MockHttpServletRequestBuilder}, the builder returned by this + * method will always create a new {@link MockMultipartHttpServletRequest} + * that is not associated with a mock request created by the + * Spring TestContext Framework. * @param urlTemplate a URL template; the resulting URL will be encoded * @param urlVariables zero or more URL variables */ @@ -194,7 +212,12 @@ public abstract class MockMvcRequestBuilders { } /** - * Create a {@link MockHttpServletRequestBuilder} for a multipart request. + * Create a {@link MockMultipartHttpServletRequestBuilder} for a multipart request. + *

In contrast to methods in this class that return a + * {@link MockHttpServletRequestBuilder}, the builder returned by this + * method will always create a new {@link MockMultipartHttpServletRequest} + * that is not associated with a mock request created by the + * Spring TestContext Framework. * @param uri the URL * @since 4.0.3 */ diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/CustomRequestAttributesRequestContextHolderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/CustomRequestAttributesRequestContextHolderTests.java new file mode 100644 index 00000000000..4df80b9d310 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/CustomRequestAttributesRequestContextHolderTests.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.web.servlet.samples.spr; + +import javax.servlet.ServletContext; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.context.web.ServletTestExecutionListener; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.support.GenericWebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; + +/** + * Integration tests for SPR-13211 which verify that a custom mock request + * (i.e., one not created by {@link ServletTestExecutionListener}) is not + * reused by MockMvc. + * + * @author Sam Brannen + * @since 4.2 + * @see RequestContextHolderTests + */ +public class CustomRequestAttributesRequestContextHolderTests { + + private static final String FROM_CUSTOM_MOCK = "fromCustomMock"; + private static final String FROM_MVC_TEST_DEFAULT = "fromSpringMvcTestDefault"; + private static final String FROM_MVC_TEST_MOCK = "fromSpringMvcTestMock"; + + private final GenericWebApplicationContext wac = new GenericWebApplicationContext(); + + private MockMvc mockMvc; + + + @Before + public void setUp() { + ServletContext servletContext = new MockServletContext(); + MockHttpServletRequest mockRequest = new MockHttpServletRequest(servletContext); + mockRequest.setAttribute(FROM_CUSTOM_MOCK, FROM_CUSTOM_MOCK); + RequestContextHolder.setRequestAttributes(new ServletWebRequest(mockRequest, new MockHttpServletResponse())); + + this.wac.setServletContext(servletContext); + new AnnotatedBeanDefinitionReader(this.wac).register(WebConfig.class); + this.wac.refresh(); + + this.mockMvc = webAppContextSetup(this.wac) + .defaultRequest(get("/").requestAttr(FROM_MVC_TEST_DEFAULT, FROM_MVC_TEST_DEFAULT)) + .alwaysExpect(status().isOk()) + .build(); + } + + @Test + public void singletonController() throws Exception { + this.mockMvc.perform(get("/singletonController").requestAttr(FROM_MVC_TEST_MOCK, FROM_MVC_TEST_MOCK)); + } + + @After + public void verifyCustomRequestAttributesAreRestored() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + assertThat(requestAttributes, instanceOf(ServletRequestAttributes.class)); + HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); + + assertThat(request.getAttribute(FROM_CUSTOM_MOCK), is(FROM_CUSTOM_MOCK)); + assertThat(request.getAttribute(FROM_MVC_TEST_DEFAULT), is(nullValue())); + assertThat(request.getAttribute(FROM_MVC_TEST_MOCK), is(nullValue())); + + RequestContextHolder.resetRequestAttributes(); + this.wac.close(); + } + + + // ------------------------------------------------------------------- + + @Configuration + @EnableWebMvc + static class WebConfig extends WebMvcConfigurerAdapter { + + @Bean + public SingletonController singletonController() { + return new SingletonController(); + } + } + + @RestController + private static class SingletonController { + + @RequestMapping("/singletonController") + public void handle() { + assertRequestAttributes(); + } + } + + private static void assertRequestAttributes() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + assertThat(requestAttributes, instanceOf(ServletRequestAttributes.class)); + assertRequestAttributes(((ServletRequestAttributes) requestAttributes).getRequest()); + } + + private static void assertRequestAttributes(ServletRequest request) { + assertThat(request.getAttribute(FROM_CUSTOM_MOCK), is(nullValue())); + assertThat(request.getAttribute(FROM_MVC_TEST_DEFAULT), is(FROM_MVC_TEST_DEFAULT)); + assertThat(request.getAttribute(FROM_MVC_TEST_MOCK), is(FROM_MVC_TEST_MOCK)); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/RequestContextHolderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/RequestContextHolderTests.java index 8ad73beee8a..6afc3f626f0 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/RequestContextHolderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/spr/RequestContextHolderTests.java @@ -23,6 +23,7 @@ import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -56,12 +57,16 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; /** - * Tests for SPR-10025 (access to request attributes via RequestContextHolder), - * SPR-13217 (Populate RequestAttributes before invoking Filters in MockMvc), - * and SPR-13211 (re-use of mock request from the TestContext framework). + * Integration tests for the following use cases. + *

* * @author Rossen Stoyanchev * @author Sam Brannen + * @see CustomRequestAttributesRequestContextHolderTests */ @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @@ -130,6 +135,11 @@ public class RequestContextHolderTests { this.mockMvc.perform(get("/sessionScopedService").requestAttr(FROM_MVC_TEST_MOCK, FROM_MVC_TEST_MOCK)); } + @After + public void verifyRestoredRequestAttributes() { + assertRequestAttributes(); + } + // ------------------------------------------------------------------- @@ -290,8 +300,7 @@ public class RequestContextHolderTests { } private static void assertRequestAttributes(ServletRequest request) { - // TODO [SPR-13211] Assert that FROM_TCF_MOCK is FROM_TCF_MOCK, instead of NULL. - assertThat(request.getAttribute(FROM_TCF_MOCK), is(nullValue())); + assertThat(request.getAttribute(FROM_TCF_MOCK), is(FROM_TCF_MOCK)); assertThat(request.getAttribute(FROM_MVC_TEST_DEFAULT), is(FROM_MVC_TEST_DEFAULT)); assertThat(request.getAttribute(FROM_MVC_TEST_MOCK), is(FROM_MVC_TEST_MOCK)); assertThat(request.getAttribute(FROM_REQUEST_FILTER), is(FROM_REQUEST_FILTER));