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}