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