1 概述
这里的白盒测试是指开发编写测试代码来进行测试,集成测试是指从Controller开始对http接口调用的整个流程进行测试。这个流程就是对一个http请求的响应流程,正常运行的时候是通过springboot内嵌的tomcat来启动一个web server来监听http请求,然后响应该http请求。在测试的时候,如果也需要启动一个web server来监听请求,那么测试就更加困难了一些。还好spring-test为这个场景提供了便利,本文来了解一下它们的原理。
2 原理
2.1 测试例子
下面是针对Controller里的sayHello接口进行的测试:
// HelloController.java
@RestController
public class HelloController {
private Logger logger = LoggerFactory.getLogger(HelloController.class);
@RequestMapping("sayHello")
public String say(@RequestParam("message") String messge) {
return "Hello world: " + messge;
}
}
// HelloControllerTest.java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
public class HelloControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void should_say_string() throws Exception {
String messge = "abc";
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/sayHello")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.content("message=" + messge))
.andExpect(status().isOk())
.andReturn();
assertThat(result.getResponse().getContentAsString()).isEqualTo("Hello world: " + messge);
}
}
2.2 http请求流程
正常的http请求流程是:springboot启动进行初始化,把关键的DispatcherServlet和Tomcat server初始化,把DispatcherServlet设置到Tomcat server中,由Tomcat server监听到http请求,然后在worker线程中由DispatcherServlet处理请求,最终调到Controller的接口执行业务逻辑。
// 1. 在自动配置的时候,DispatcherServlet成为一个bean,注入到DispatcherServletRegistrationBean中,再通过DispatcherServletRegistrationBean设置到tomcat中
// 源码位置:org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration.DispatcherServletRegistrationConfiguration
@Configuration(proxyBeanMethods = false)
@Conditional(DispatcherServletRegistrationCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
@Import(DispatcherServletConfiguration.class)
protected static class DispatcherServletRegistrationConfiguration {
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
// 2. DispatcherServletRegistrationBean实现了org.springframework.boot.web.servlet.ServletContextInitializer接口,
// 后面会通过这个接口类型来获取此bean
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
// 省略其它代码
}
// 源码位置:org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
@Configuration(proxyBeanMethods = false)
@Conditional(DefaultDispatcherServletCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
protected static class DispatcherServletConfiguration {
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
// 3. 初始化org.springframework.web.servlet.DispatcherServlet为一个bean
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
return dispatcherServlet;
}
// 省略其它代码
}
// 4. Springboot启动的时候,会初始化ServletWebServerApplicationContext
// 源码位置:org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext
private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
return this::selfInitialize;
}
private void selfInitialize(ServletContext servletContext) throws ServletException {
prepareWebApplicationContext(servletContext); // servletContext为ApplicationContextFacade
registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
// 5. getServletContextInitializerBeans()获取实现了ServletContextInitializer接口的bean,以获得里面的dispatcherServlet
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
// 6. 会调用ApplicationContextFacade的addServlet()把dispatcherServlet设置到tomcat中
beans.onStartup(servletContext);
}
}
protected Collection<ServletContextInitializer> getServletContextInitializerBeans() {
return new ServletContextInitializerBeans(getBeanFactory());
}
// 源码位置:org.springframework.boot.web.servlet.ServletContextInitializerBeans
public ServletContextInitializerBeans(ListableBeanFactory beanFactory, Class<? extends ServletContextInitializer>... initializerTypes) {
this.initializers = new LinkedMultiValueMap<>();
this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes) : Collections.singletonList(ServletContextInitializer.class);
addServletContextInitializerBeans(beanFactory);
// 省略其它代码
}
private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) {
for (Class<? extends ServletContextInitializer> initializerType : this.initializerTypes) {
// 7. initializerType=org.springframework.boot.web.servlet.ServletContextInitializer
// DispatcherServletRegistrationBean实现了ServletContextInitializer接口,根据该接口类型获取到此bean,
// 目的是获取里面的dispatcherServlet,存到ServletContextInitializerBeans中
for (Entry<String, ? extends ServletContextInitializer> initializerBean : getOrderedBeansOfType(beanFactory, initializerType)) {
// key=dispatcherServletRegistration,value=dispatcherServlet
addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory);
}
}
}
// 源码位置:org.apache.catalina.core.ApplicationContextFacade
public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) {
if (SecurityUtil.isPackageProtectionEnabled()) {
return (ServletRegistration.Dynamic) doPrivileged("addServlet", new Class[] { String.class, Servlet.class },
new Object[] { servletName, servlet });
} else {
// 8. context为tomcat里的org.apache.catalina.core.ApplicationContext
// 即把dispatcherServlet塞到了tomcat里执行
return context.addServlet(servletName, servlet);
}
}
// 9. 在发起http请求的时候,tomcat会创建ApplicationFilterChain
// 源码位置:org.apache.catalina.core.ApplicationFilterFactory(tomcat-embed-core包)
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
// If there is no servlet to execute, return null
if (servlet == null) {
return null;
}
// Create and initialize a filter chain object
ApplicationFilterChain filterChain = null;
if (request instanceof Request) {
Request req = (Request) request;
if (Globals.IS_SECURITY_ENABLED) {
// Security: Do not recycle
filterChain = new ApplicationFilterChain();
} else {
filterChain = (ApplicationFilterChain) req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
// Request dispatcher in use
// 10. 创建ApplicationFilterChain(实现javax.servlet.FilterChain接口)
filterChain = new ApplicationFilterChain();
}
// 11. 把dispatcherServlet设置到ApplicationFilterChain中
filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
// 省略其它代码
// Return the completed filter chain
return filterChain;
}
// 12. tomcat启动worker线程执行ApplicationFilterChain的service方法
// 源码位置:org.apache.catalina.core.ApplicationFilterChain(tomcat-embed-core包)
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (Globals.IS_SECURITY_ENABLED) {
final ServletRequest req = request;
final ServletResponse res = response;
try {
java.security.AccessController.doPrivileged((java.security.PrivilegedExceptionAction<Void>) () -> {
internalDoFilter(req, res);
return null;
});
} catch (PrivilegedActionException pe) {
Exception e = pe.getException();
if (e instanceof ServletException) {
throw (ServletException) e;
} else if (e instanceof IOException) {
throw (IOException) e;
} else if (e instanceof RuntimeException) {
throw (RuntimeException) e;
} else {
throw new ServletException(e.getMessage(), e);
}
}
} else {
// 13. 调用internalDoFilter()执行过滤器
internalDoFilter(request, response);
}
}
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
// 省略其它代码
try {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
if (request.isAsyncSupported() && !servletSupportsAsync) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
}
// Use potentially wrapped request from this point
if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) &&
Globals.IS_SECURITY_ENABLED) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal = ((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[] { req, res };
SecurityUtil.doAsPrivilege("service", servlet, classTypeUsedInService, args, principal);
} else {
// 14. 此servlet为dispatcherServlet,执行dispatcherServlet的service()
servlet.service(request, response);
}
} catch (IOException | ServletException | RuntimeException e) {
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.servlet"), e);
} finally {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(null);
lastServicedResponse.set(null);
}
}
}
2.3 测试流程
在执行测试用例的时候,从DispatcherServlet中扩展出一个TestDispatcherServlet,放到一个mock的MockMvc对象中;在执行具体测试用例时,调MockMvc的perform()方法来模拟发http请求,该请求没有tomcat server来接收,而是封装到一个mock请求中,用一个MockFilterChain来模仿filter链执行,最终执行到DispatcherServlet的service()方法,给予一个mock的响应。
// 1. 初始化的时候,创建个MockMvc类型的bean,里面包含着TestDispatcherServlet对象,TestDispatcherServlet继承于DispatcherServlet
// 源码位置:org.springframework.boot.test.autoconfigure.web.servlet.MockMvcAutoConfiguration
@Bean
@ConditionalOnMissingBean
public MockMvc mockMvc(MockMvcBuilder builder) {
// 2. 用builder去创建MockMvc
return builder.build();
}
// 源码位置:org.springframework.test.web.servlet.setup.AbstractMockMvcBuilder
public final MockMvc build() {
WebApplicationContext wac = initWebAppContext();
ServletContext servletContext = wac.getServletContext();
MockServletConfig mockServletConfig = new MockServletConfig(servletContext);
for (MockMvcConfigurer configurer : this.configurers) {
RequestPostProcessor processor = configurer.beforeMockMvcCreated(this, wac);
if (processor != null) {
if (this.defaultRequestBuilder == null) {
this.defaultRequestBuilder = MockMvcRequestBuilders.get("/");
}
if (this.defaultRequestBuilder instanceof ConfigurableSmartRequestBuilder) {
((ConfigurableSmartRequestBuilder) this.defaultRequestBuilder).with(processor);
}
}
}
Filter[] filterArray = this.filters.toArray(new Filter[0]);
// 3. 创建MockMvc
return super.createMockMvc(filterArray, mockServletConfig, wac, this.defaultRequestBuilder,
this.defaultResponseCharacterEncoding, this.globalResultMatchers, this.globalResultHandlers,
this.dispatcherServletCustomizers);
}
// 源码位置:org.springframework.test.web.servlet.MockMvcBuilderSupport
protected final MockMvc createMockMvc(Filter[] filters, MockServletConfig servletConfig,
WebApplicationContext webAppContext, @Nullable RequestBuilder defaultRequestBuilder,
@Nullable Charset defaultResponseCharacterEncoding,
List<ResultMatcher> globalResultMatchers, List<ResultHandler> globalResultHandlers,
@Nullable List<DispatcherServletCustomizer> dispatcherServletCustomizers) {
// 4. 创建MockMvc
MockMvc mockMvc = createMockMvc(filters, servletConfig, webAppContext, defaultRequestBuilder, globalResultMatchers, globalResultHandlers, dispatcherServletCustomizers);
mockMvc.setDefaultResponseCharacterEncoding(defaultResponseCharacterEncoding);
return mockMvc;
}
// 源码位置:org.springframework.test.web.servlet.MockMvcBuilderSupport
protected final MockMvc createMockMvc(Filter[] filters, MockServletConfig servletConfig,
WebApplicationContext webAppContext, @Nullable RequestBuilder defaultRequestBuilder,
List<ResultMatcher> globalResultMatchers, List<ResultHandler> globalResultHandlers,
@Nullable List<DispatcherServletCustomizer> dispatcherServletCustomizers) {
// 5. 创建一个mock的TestDispatcherServlet,其继承于DispatcherServlet
TestDispatcherServlet dispatcherServlet = new TestDispatcherServlet(webAppContext);
if (dispatcherServletCustomizers != null) {
for (DispatcherServletCustomizer customizers : dispatcherServletCustomizers) {
customizers.customize(dispatcherServlet);
}
}
try {
dispatcherServlet.init(servletConfig);
}
catch (ServletException ex) {
// should never happen..
throw new MockMvcBuildException("Failed to initialize TestDispatcherServlet", ex);
}
// 6. TestDispatcherServlet对象存储到MockMvc对象中
MockMvc mockMvc = new MockMvc(dispatcherServlet, filters);
mockMvc.setDefaultRequest(defaultRequestBuilder);
mockMvc.setGlobalResultMatchers(globalResultMatchers);
mockMvc.setGlobalResultHandlers(globalResultHandlers);
return mockMvc;
}
// 7. 调用mockMvc.perform()的时候
// 源码位置:org.springframework.test.web.servlet.MockMvc
public ResultActions perform(RequestBuilder requestBuilder) throws Exception {
if (this.defaultRequestBuilder != null && requestBuilder instanceof Mergeable) {
requestBuilder = (RequestBuilder) ((Mergeable) requestBuilder).merge(this.defaultRequestBuilder);
}
// 8. mock个请求Request
MockHttpServletRequest request = requestBuilder.buildRequest(this.servletContext);
AsyncContext asyncContext = request.getAsyncContext();
// 9. mock个响应Response
MockHttpServletResponse mockResponse;
HttpServletResponse servletResponse;
if (asyncContext != null) {
servletResponse = (HttpServletResponse) asyncContext.getResponse();
mockResponse = unwrapResponseIfNecessary(servletResponse);
}
else {
mockResponse = new MockHttpServletResponse();
servletResponse = mockResponse;
}
if (this.defaultResponseCharacterEncoding != null) {
mockResponse.setDefaultCharacterEncoding(this.defaultResponseCharacterEncoding.name());
}
if (requestBuilder instanceof SmartRequestBuilder) {
request = ((SmartRequestBuilder) requestBuilder).postProcessRequest(request);
}
MvcResult mvcResult = new DefaultMvcResult(request, mockResponse);
request.setAttribute(MVC_RESULT_ATTRIBUTE, mvcResult);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, servletResponse));
// 10. 创建个MockFilterChain(实现javax.servlet.FilterChain接口),把TestDispatcherServlet对象放进去
MockFilterChain filterChain = new MockFilterChain(this.servlet, this.filters);
// 11. 执行FilterChain的doFilter()接口,MockFilterChain的内部类ServletFilterProxy也是个filter,遍历filter会调到ServletFilterProxy这个filter
filterChain.doFilter(request, servletResponse);
if (DispatcherType.ASYNC.equals(request.getDispatcherType()) &&
asyncContext != null && !request.isAsyncStarted()) {
asyncContext.complete();
}
applyDefaultResultActions(mvcResult);
RequestContextHolder.setRequestAttributes(previousAttributes);
return new ResultActions() {
@Override
public ResultActions andExpect(ResultMatcher matcher) throws Exception {
matcher.match(mvcResult);
return this;
}
@Override
public ResultActions andDo(ResultHandler handler) throws Exception {
handler.handle(mvcResult);
return this;
}
@Override
public MvcResult andReturn() {
return mvcResult;
}
};
}
// 12. MockFilterChain的内部类ServletFilterProxy也是个filter,执行MockFilterChain里的filter时会调到
// 源码位置:org.springframework.mock.web.MockFilterChain.ServletFilterProxy
private static final class ServletFilterProxy implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 13. delegateServlet就是TestDispatcherServlet,执行其service()方法
this.delegateServlet.service(request, response);
}
}
// 源码位置:org.springframework.test.web.servlet.TestDispatcherServlet
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
registerAsyncResultInterceptors(request);
// 14. 其父类为DispatcherServlet(org.springframework.web.servlet.DispatcherServlet),调用DispatcherServlet的service()方法
super.service(request, response);
if (request.getAsyncContext() != null) {
MockAsyncContext asyncContext;
if (request.getAsyncContext() instanceof MockAsyncContext) {
asyncContext = (MockAsyncContext) request.getAsyncContext();
}
else {
MockHttpServletRequest mockRequest = WebUtils.getNativeRequest(request, MockHttpServletRequest.class);
Assert.notNull(mockRequest, "Expected MockHttpServletRequest");
asyncContext = (MockAsyncContext) mockRequest.getAsyncContext();
String requestClassName = request.getClass().getName();
Assert.notNull(asyncContext, () ->
"Outer request wrapper " + requestClassName + " has an AsyncContext," +
"but it is not a MockAsyncContext, while the nested " +
mockRequest.getClass().getName() + " does not have an AsyncContext at all.");
}
CountDownLatch dispatchLatch = new CountDownLatch(1);
asyncContext.addDispatchHandler(dispatchLatch::countDown);
getMvcResult(request).setAsyncDispatchLatch(dispatchLatch);
}
}
可见,在测试的过程中,省掉了tomcat server的过程,而直接调DispatcherServlet的service()方法来模仿http请求的响应。整个过程需要依赖springboot的执行,所以需要有@SpringBootTest注解,里面指定了springboot的基础执行,同时要提供一个MockMvc对象来总控该流程,所以需要@AutoConfigureMockMvc注解才能自动创建MockMvc对象。
3 架构一小步
1、使用@SpringBootTest、@AutoConfigureMockMvc注解来进行http请求集成测试。
2、http请求集成测试可以注入MockMvc对象,调用该对象的perform()接口来模仿http请求响应。