小架构step系列14:白盒集成测试原理
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 {@Autowiredprivate MockMvc mockMvc;@Testpublic 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接口,// 后面会通过这个接口类型来获取此beanDispatcherServletRegistrationBean 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为一个beanDispatcherServlet 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为ApplicationContextFacaderegisterApplicationScope(servletContext);WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);// 5. getServletContextInitializerBeans()获取实现了ServletContextInitializer接口的bean,以获得里面的dispatcherServletfor (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=dispatcherServletaddServletContextInitializerBean(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 nullif (servlet == null) {return null;}// Create and initialize a filter chain objectApplicationFilterChain filterChain = null;if (request instanceof Request) {Request req = (Request) request;if (Globals.IS_SECURITY_ENABLED) {// Security: Do not recyclefilterChain = 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 chainreturn 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 pointif ((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去创建MockMvcreturn 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. 创建MockMvcreturn 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. 创建MockMvcMockMvc 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,其继承于DispatcherServletTestDispatcherServlet 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个请求RequestMockHttpServletRequest request = requestBuilder.buildRequest(this.servletContext);AsyncContext asyncContext = request.getAsyncContext();// 9. mock个响应ResponseMockHttpServletResponse 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这个filterfilterChain.doFilter(request, servletResponse);if (DispatcherType.ASYNC.equals(request.getDispatcherType()) &&asyncContext != null && !request.isAsyncStarted()) {asyncContext.complete();}applyDefaultResultActions(mvcResult);RequestContextHolder.setRequestAttributes(previousAttributes);return new ResultActions() {@Overridepublic ResultActions andExpect(ResultMatcher matcher) throws Exception {matcher.match(mvcResult);return this;}@Overridepublic ResultActions andDo(ResultHandler handler) throws Exception {handler.handle(mvcResult);return this;}@Overridepublic 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请求响应。