001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.test.context.web;
018
019import javax.servlet.ServletContext;
020
021import org.apache.commons.logging.Log;
022import org.apache.commons.logging.LogFactory;
023
024import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
025import org.springframework.context.ApplicationContext;
026import org.springframework.context.ConfigurableApplicationContext;
027import org.springframework.core.Conventions;
028import org.springframework.core.annotation.AnnotatedElementUtils;
029import org.springframework.mock.web.MockHttpServletRequest;
030import org.springframework.mock.web.MockHttpServletResponse;
031import org.springframework.mock.web.MockServletContext;
032import org.springframework.test.context.TestContext;
033import org.springframework.test.context.TestExecutionListener;
034import org.springframework.test.context.support.AbstractTestExecutionListener;
035import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
036import org.springframework.util.Assert;
037import org.springframework.web.context.WebApplicationContext;
038import org.springframework.web.context.request.RequestAttributes;
039import org.springframework.web.context.request.RequestContextHolder;
040import org.springframework.web.context.request.ServletWebRequest;
041
042/**
043 * {@code TestExecutionListener} which provides mock Servlet API support to
044 * {@link WebApplicationContext WebApplicationContexts} loaded by the <em>Spring
045 * TestContext Framework</em>.
046 *
047 * <p>Specifically, {@code ServletTestExecutionListener} sets up thread-local
048 * state via Spring Web's {@link RequestContextHolder} during {@linkplain
049 * #prepareTestInstance(TestContext) test instance preparation} and {@linkplain
050 * #beforeTestMethod(TestContext) before each test method} and creates a {@link
051 * MockHttpServletRequest}, {@link MockHttpServletResponse}, and
052 * {@link ServletWebRequest} based on the {@link MockServletContext} present in
053 * the {@code WebApplicationContext}. This listener also ensures that the
054 * {@code MockHttpServletResponse} and {@code ServletWebRequest} can be injected
055 * into the test instance, and once the test is complete this listener {@linkplain
056 * #afterTestMethod(TestContext) cleans up} thread-local state.
057 *
058 * <p>Note that {@code ServletTestExecutionListener} is enabled by default but
059 * generally takes no action if the {@linkplain TestContext#getTestClass() test
060 * class} is not annotated with {@link WebAppConfiguration @WebAppConfiguration}.
061 * See the javadocs for individual methods in this class for details.
062 *
063 * @author Sam Brannen
064 * @author Phillip Webb
065 * @since 3.2
066 */
067public class ServletTestExecutionListener extends AbstractTestExecutionListener {
068
069        /**
070         * Attribute name for a {@link TestContext} attribute which indicates
071         * whether or not the {@code ServletTestExecutionListener} should {@linkplain
072         * RequestContextHolder#resetRequestAttributes() reset} Spring Web's
073         * {@code RequestContextHolder} in {@link #afterTestMethod(TestContext)}.
074         * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}.
075         */
076        public static final String RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE = Conventions.getQualifiedAttributeName(
077                        ServletTestExecutionListener.class, "resetRequestContextHolder");
078
079        /**
080         * Attribute name for a {@link TestContext} attribute which indicates that
081         * {@code ServletTestExecutionListener} has already populated Spring Web's
082         * {@code RequestContextHolder}.
083         * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}.
084         */
085        public static final String POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE = Conventions.getQualifiedAttributeName(
086                        ServletTestExecutionListener.class, "populatedRequestContextHolder");
087
088        /**
089         * Attribute name for a request attribute which indicates that the
090         * {@link MockHttpServletRequest} stored in the {@link RequestAttributes}
091         * in Spring Web's {@link RequestContextHolder} was created by the TestContext
092         * framework.
093         * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}.
094         * @since 4.2
095         */
096        public static final String CREATED_BY_THE_TESTCONTEXT_FRAMEWORK = Conventions.getQualifiedAttributeName(
097                        ServletTestExecutionListener.class, "createdByTheTestContextFramework");
098
099        /**
100         * Attribute name for a {@link TestContext} attribute which indicates that the
101         * {@code ServletTestExecutionListener} should be activated. When not set to
102         * {@code true}, activation occurs when the {@linkplain TestContext#getTestClass()
103         * test class} is annotated with {@link WebAppConfiguration @WebAppConfiguration}.
104         * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}.
105         * @since 4.3
106         */
107        public static final String ACTIVATE_LISTENER = Conventions.getQualifiedAttributeName(
108                        ServletTestExecutionListener.class, "activateListener");
109
110
111        private static final Log logger = LogFactory.getLog(ServletTestExecutionListener.class);
112
113
114        /**
115         * Returns {@code 1000}.
116         */
117        @Override
118        public final int getOrder() {
119                return 1000;
120        }
121
122        /**
123         * Sets up thread-local state during the <em>test instance preparation</em>
124         * callback phase via Spring Web's {@link RequestContextHolder}, but only if
125         * the {@linkplain TestContext#getTestClass() test class} is annotated with
126         * {@link WebAppConfiguration @WebAppConfiguration}.
127         * @see TestExecutionListener#prepareTestInstance(TestContext)
128         * @see #setUpRequestContextIfNecessary(TestContext)
129         */
130        @Override
131        public void prepareTestInstance(TestContext testContext) throws Exception {
132                setUpRequestContextIfNecessary(testContext);
133        }
134
135        /**
136         * Sets up thread-local state before each test method via Spring Web's
137         * {@link RequestContextHolder}, but only if the
138         * {@linkplain TestContext#getTestClass() test class} is annotated with
139         * {@link WebAppConfiguration @WebAppConfiguration}.
140         * @see TestExecutionListener#beforeTestMethod(TestContext)
141         * @see #setUpRequestContextIfNecessary(TestContext)
142         */
143        @Override
144        public void beforeTestMethod(TestContext testContext) throws Exception {
145                setUpRequestContextIfNecessary(testContext);
146        }
147
148        /**
149         * If the {@link #RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} in the supplied
150         * {@code TestContext} has a value of {@link Boolean#TRUE}, this method will
151         * (1) clean up thread-local state after each test method by {@linkplain
152         * RequestContextHolder#resetRequestAttributes() resetting} Spring Web's
153         * {@code RequestContextHolder} and (2) ensure that new mocks are injected
154         * into the test instance for subsequent tests by setting the
155         * {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE}
156         * in the test context to {@code true}.
157         * <p>The {@link #RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} and
158         * {@link #POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} will be subsequently
159         * removed from the test context, regardless of their values.
160         * @see TestExecutionListener#afterTestMethod(TestContext)
161         */
162        @Override
163        public void afterTestMethod(TestContext testContext) throws Exception {
164                if (Boolean.TRUE.equals(testContext.getAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE))) {
165                        if (logger.isDebugEnabled()) {
166                                logger.debug(String.format("Resetting RequestContextHolder for test context %s.", testContext));
167                        }
168                        RequestContextHolder.resetRequestAttributes();
169                        testContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE,
170                                Boolean.TRUE);
171                }
172                testContext.removeAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE);
173                testContext.removeAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE);
174        }
175
176        private boolean isActivated(TestContext testContext) {
177                return (Boolean.TRUE.equals(testContext.getAttribute(ACTIVATE_LISTENER)) ||
178                                AnnotatedElementUtils.hasAnnotation(testContext.getTestClass(), WebAppConfiguration.class));
179        }
180
181        private boolean alreadyPopulatedRequestContextHolder(TestContext testContext) {
182                return Boolean.TRUE.equals(testContext.getAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE));
183        }
184
185        private void setUpRequestContextIfNecessary(TestContext testContext) {
186                if (!isActivated(testContext) || alreadyPopulatedRequestContextHolder(testContext)) {
187                        return;
188                }
189
190                ApplicationContext context = testContext.getApplicationContext();
191
192                if (context instanceof WebApplicationContext) {
193                        WebApplicationContext wac = (WebApplicationContext) context;
194                        ServletContext servletContext = wac.getServletContext();
195                        Assert.state(servletContext instanceof MockServletContext, () -> String.format(
196                                                "The WebApplicationContext for test context %s must be configured with a MockServletContext.",
197                                                testContext));
198
199                        if (logger.isDebugEnabled()) {
200                                logger.debug(String.format(
201                                                "Setting up MockHttpServletRequest, MockHttpServletResponse, ServletWebRequest, and RequestContextHolder for test context %s.",
202                                                testContext));
203                        }
204
205                        MockServletContext mockServletContext = (MockServletContext) servletContext;
206                        MockHttpServletRequest request = new MockHttpServletRequest(mockServletContext);
207                        request.setAttribute(CREATED_BY_THE_TESTCONTEXT_FRAMEWORK, Boolean.TRUE);
208                        MockHttpServletResponse response = new MockHttpServletResponse();
209                        ServletWebRequest servletWebRequest = new ServletWebRequest(request, response);
210
211                        RequestContextHolder.setRequestAttributes(servletWebRequest);
212                        testContext.setAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE);
213                        testContext.setAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE);
214
215                        if (wac instanceof ConfigurableApplicationContext) {
216                                @SuppressWarnings("resource")
217                                ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) wac;
218                                ConfigurableListableBeanFactory bf = configurableApplicationContext.getBeanFactory();
219                                bf.registerResolvableDependency(MockHttpServletResponse.class, response);
220                                bf.registerResolvableDependency(ServletWebRequest.class, servletWebRequest);
221                        }
222                }
223        }
224
225}