/*
* Copyright 2002-2017 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.web;
import javax.servlet.ServletContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.Conventions;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
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;
/**
* {@code TestExecutionListener} which provides mock Servlet API support to
* {@link WebApplicationContext WebApplicationContexts} loaded by the <em>Spring
* TestContext Framework</em>.
*
* <p>Specifically, {@code ServletTestExecutionListener} sets up thread-local
* state via Spring Web's {@link RequestContextHolder} during {@linkplain
* #prepareTestInstance(TestContext) test instance preparation} and {@linkplain
* #beforeTestMethod(TestContext) before each test method} and creates a {@link
* MockHttpServletRequest}, {@link MockHttpServletResponse}, and
* {@link ServletWebRequest} based on the {@link MockServletContext} present in
* the {@code WebApplicationContext}. This listener also ensures that the
* {@code MockHttpServletResponse} and {@code ServletWebRequest} can be injected
* into the test instance, and once the test is complete this listener {@linkplain
* #afterTestMethod(TestContext) cleans up} thread-local state.
*
* <p>Note that {@code ServletTestExecutionListener} is enabled by default but
* generally takes no action if the {@linkplain TestContext#getTestClass() test
* class} is not annotated with {@link WebAppConfiguration @WebAppConfiguration}.
* See the javadocs for individual methods in this class for details.
*
* @author Sam Brannen
* @author Phillip Webb
* @since 3.2
*/
public class ServletTestExecutionListener extends AbstractTestExecutionListener {
/**
* Attribute name for a {@link TestContext} attribute which indicates
* whether or not the {@code ServletTestExecutionListener} should {@linkplain
* RequestContextHolder#resetRequestAttributes() reset} Spring Web's
* {@code RequestContextHolder} in {@link #afterTestMethod(TestContext)}.
* <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}.
*/
public static final String RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE = Conventions.getQualifiedAttributeName(
ServletTestExecutionListener.class, "resetRequestContextHolder");
/**
* Attribute name for a {@link TestContext} attribute which indicates that
* {@code ServletTestExecutionListener} has already populated Spring Web's
* {@code RequestContextHolder}.
* <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}.
*/
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.
* <p>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");
/**
* Attribute name for a {@link TestContext} attribute which indicates that the
* {@code ServletTestExecutionListener} should be activated. When not set to
* {@code true}, activation occurs when the {@linkplain TestContext#getTestClass()
* test class} is annotated with {@link WebAppConfiguration @WebAppConfiguration}.
* <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}.
* @since 4.3
*/
public static final String ACTIVATE_LISTENER = Conventions.getQualifiedAttributeName(
ServletTestExecutionListener.class, "activateListener");
private static final Log logger = LogFactory.getLog(ServletTestExecutionListener.class);
/**
* Returns {@code 1000}.
*/
@Override
public final int getOrder() {
return 1000;
}
/**
* Sets up thread-local state during the <em>test instance preparation</em>
* callback phase via Spring Web's {@link RequestContextHolder}, but only if
* the {@linkplain TestContext#getTestClass() test class} is annotated with
* {@link WebAppConfiguration @WebAppConfiguration}.
* @see TestExecutionListener#prepareTestInstance(TestContext)
* @see #setUpRequestContextIfNecessary(TestContext)
*/
@Override
public void prepareTestInstance(TestContext testContext) throws Exception {
setUpRequestContextIfNecessary(testContext);
}
/**
* Sets up thread-local state before each test method via Spring Web's
* {@link RequestContextHolder}, but only if the
* {@linkplain TestContext#getTestClass() test class} is annotated with
* {@link WebAppConfiguration @WebAppConfiguration}.
* @see TestExecutionListener#beforeTestMethod(TestContext)
* @see #setUpRequestContextIfNecessary(TestContext)
*/
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
setUpRequestContextIfNecessary(testContext);
}
/**
* If the {@link #RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} in the supplied
* {@code TestContext} has a value of {@link Boolean#TRUE}, this method will
* (1) clean up thread-local state after each test method by {@linkplain
* RequestContextHolder#resetRequestAttributes() resetting} Spring Web's
* {@code RequestContextHolder} and (2) ensure that new mocks are injected
* into the test instance for subsequent tests by setting the
* {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE}
* in the test context to {@code true}.
* <p>The {@link #RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} and
* {@link #POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} will be subsequently
* removed from the test context, regardless of their values.
* @see TestExecutionListener#afterTestMethod(TestContext)
*/
@Override
public void afterTestMethod(TestContext testContext) throws Exception {
if (Boolean.TRUE.equals(testContext.getAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE))) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("Resetting RequestContextHolder for test context %s.", testContext));
}
RequestContextHolder.resetRequestAttributes();
testContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE,
Boolean.TRUE);
}
testContext.removeAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE);
testContext.removeAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE);
}
private boolean isActivated(TestContext testContext) {
return (Boolean.TRUE.equals(testContext.getAttribute(ACTIVATE_LISTENER)) ||
AnnotatedElementUtils.hasAnnotation(testContext.getTestClass(), WebAppConfiguration.class));
}
private boolean alreadyPopulatedRequestContextHolder(TestContext testContext) {
return Boolean.TRUE.equals(testContext.getAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE));
}
private void setUpRequestContextIfNecessary(TestContext testContext) {
if (!isActivated(testContext) || alreadyPopulatedRequestContextHolder(testContext)) {
return;
}
ApplicationContext context = testContext.getApplicationContext();
if (context instanceof WebApplicationContext) {
WebApplicationContext wac = (WebApplicationContext) context;
ServletContext servletContext = wac.getServletContext();
Assert.state(servletContext instanceof MockServletContext, () -> String.format(
"The WebApplicationContext for test context %s must be configured with a MockServletContext.",
testContext));
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"Setting up MockHttpServletRequest, MockHttpServletResponse, ServletWebRequest, and RequestContextHolder for test context %s.",
testContext));
}
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);
RequestContextHolder.setRequestAttributes(servletWebRequest);
testContext.setAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE);
testContext.setAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE);
if (wac instanceof ConfigurableApplicationContext) {
@SuppressWarnings("resource")
ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) wac;
ConfigurableListableBeanFactory bf = configurableApplicationContext.getBeanFactory();
bf.registerResolvableDependency(MockHttpServletResponse.class, response);
bf.registerResolvableDependency(ServletWebRequest.class, servletWebRequest);
}
}
}
}