小架构step系列14:白盒集成测试原理

发布于:2025-07-18 ⋅ 阅读:(15) ⋅ 点赞:(0)

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请求响应。


网站公告

今日签到

点亮在社区的每一天
去签到