【测试相关】通过@WebMvcTest测试Spring Boot Controller

本文使用的是Spring Boot 5.7.0 version。

  • 关于Spring Boot与JUnit的关系,参考: 【测试相关】Spring Boot测试介绍,与JUnit4, Junit5的集成
  • 关于JUnit5,参考: 【测试相关】JUnit5介绍
  • 关于Mockito,参考: 【测试相关】Java Mock框架之Mockito

    1. @WebMvcTest 注解

    官网关于Spring MVC相关的文档: https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing.spring-boot-applications.spring-mvc-tests

    用位于 spring-boot-test-autoconfigure 包中的 @WebMvcTest 注解来测试Spring MVC Controller的行为(主要是会为我们自动注入一个 MockMvc 对象,Spring MVC Server端测试的主要类, MockMvc 位于 spring-test 包中)。

    @WebMvcTest 注解通常需要传入目标Controller类,如 @WebMvcTest(BookController.class) ,即它会自动扫描BookController类以及注入Spring MVC相关的配置,扫描的配置列举如下:

    @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, WebMvcRegistrations, and HandlerMethodArgumentResolver

    如果BookController中有BookService, @WebMvcTest 并不会自动帮我们装配,需要我们通过@MockBean来声明一个Mock对象,并且使用Mockito的when/then/...来Mock BookService的行为(后面有示例)。

    【一些其它的点】

  • 值得注解的是: @Component 注解的配置以及 @ConfigurationProperties 并不会被扫描。
  • 可以使用 @EnableConfigurationProperties 来开始对 @ConfigurationProperties 的扫描。
  • @WebMvcTest 默认开启的auto-configuration,可以在 页面 中找到。
  • 另外,如果想要再额外include某些配置,可以使用 @Import
  • 想要达到 @SpringBootTest 这样的全局扫描,也可以使用 @AutoConfigureMockMvc 来代替 @WebMvcTest ,并且需要加上 @SpringBootTest 注解。
  • 1.1 @WebMvcTest vs @SpringBootTest

    在项目中新建一个Configuration:

    @Configuration
    public class BookConfiguration {
        @Bean
        public BookDomain bookDomain() {
            return new BookDomain(1001, "test bean");
    

    @SpringBootTest会自动装配@SpringBootApplication类所有的包以及子包下所有的Bean:

    @SpringBootTest
    public class BookConfigurationTest {
        @Autowired
        private BookDomain bookDomain;
        @Test
        public void beanTest() {
            Assertions.assertEquals(1001, bookDomain.getId());
            Assertions.assertEquals("test bean", bookDomain.getName());
    

    我们使用@WebMvcTest测试目标Controller:BookController,目的是为了测试会不会自动扫描@Configuration配置:

    @WebMvcTest(BookController.class)
    public class BookControllerTest {
        @Autowired
        private MockMvc mockMvc;
        @MockBean
        BookService bookService;
        @Autowired
        private BookDomain bookDomain;
        @Test
        public void test() {
    

    会报错:> nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.domain.BookDomain' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

    @WebMvcTest并不会include @Configuration的配置。

    参考:https://stackoverflow.com/questions/39865596/difference-between-using-mockmvc-with-springboottest-and-using-webmvctest

    @WebMvcTest如同上述介绍的,会扫描它定义的Controller,并且引入Spring MVC相关的配置,也会自动注入MockMvc instance,但并不会全局扫描并include所有的bean(比如不会扫描@Configuration配置)。 SpringBootTest则会启动整个Spring Application Context,所以@Configuration的配置自然也会被扫描以及注解,但并不会注入Spring MVC相关的比如MockMvc

    1.2 若想include @Configuration,可以使用AutoConfigureMockMvc

    在上述#1.1的BookControllerTest,我们无法通过@WebMvcTest(BookController.class),来include @Configuration的配置,相应的,我们可以改用@AutoConfigureMockMvc+@SpringBootTest的方式:

    @AutoConfigureMockMvc
    @SpringBootTest
    public class BookControllerTest {
        @Autowired
        private MockMvc mockMvc;
        @MockBean
        BookService bookService;
        @Autowired
        private BookDomain bookDomain;
        @Test
        public void beanTest() {
            Assertions.assertEquals(1001, bookDomain.getId());
            Assertions.assertEquals("test bean", bookDomain.getName());
    

    2. 测试get以及post APIs

    以下示例测试了BookController中的两个APIs:

  • GET - /books,返回BookDomain list。
  • POST - /books,新增Book,接收RequestBody为BookDomain。
  • 一些解释:

  • 下述中的get,post方法,是位于org.springframework.test.web.servlet.request包下,MockMvcRequestBuilders类中,都是static方法。
  • 关于BookService的行为,使用Mockito进行Mock。
  • 除了能测试response status = 200外,也能测其它错误的Response。
  • print()则是打印结果。
  • 测试MVC的写法很灵活,也可以测各种MediaType,比如application/jsontext/html等等。
  • @WebMvcTest(BookController.class)
    public class BookControllerTest {
        @Autowired
        private MockMvc mockMvc;
        @MockBean
        BookService bookService;
        @Test
        public void listTest() throws Exception {
            List<BookDomain> list = new ArrayList<>();
            list.add(new BookDomain(1, "test"));
            Mockito.when(bookService.list()).thenReturn(list);
            MvcResult mvcResult = this.mockMvc.perform(get("/books").accept(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andDo(print())
                    .andReturn();
            System.out.println(mvcResult.getResponse().getContentAsString());
            Assertions.assertTrue(mvcResult.getResponse().getContentAsString().contains("test"));
        @Test
        public void saveTest() throws Exception {
            Mockito.doNothing().when(bookService).save(Mockito.isA(BookDomain.class));
            JsonMapper jsonMapper = new JsonMapper();
            MvcResult mvcResult = this.mockMvc.perform(post("/books")
                            .contentType(MediaType.APPLICATION_JSON).content(jsonMapper.writeValueAsBytes(new BookDomain(1, "asdf"))))
                    .andExpect(status().isOk())
                    .andReturn();
            Assertions.assertEquals("true", mvcResult.getResponse().getContentAsString());
    

    3. 通过MockMvc来测试我们自定义的Filter

    假设我们有UserFilter,然后我们想要通过MockMvc来测试自定义的Filter。

    首先,自定义Filter有两种实现方式:

  • 第一种方式:在Spring Boot启动类中加@ServletComponentScan,并且在我们的UserFilter上加注解:@WebFilter(filterName = "userFilter", urlPatterns = "/*")
  • 第二种方式,以Bean的方式注入。通过FilterRegistrationBean新建Filter并返回,该方法标注为@Bean
  • 根据#1介绍的,@WebMvcTest注解只会扫描它定义的Controller类,并会忽略一切@Configuration,所以我们要使用#1.2的方式,即:@AutoConfigureMockMvc+@SpringBootTest

    以下是代码示例。

    首先新建UserFilter类:

    public class UserFilter implements Filter {
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("user filter");
            filterChain.doFilter(servletRequest, servletResponse);
    

    定义Configuration配置类:

    @Configuration
    public class FilterConfig {
        @Bean
        public FilterRegistrationBean userFilter() {
            FilterRegistrationBean registration = new FilterRegistrationBean();
            registration.setFilter(new UserFilter());
            registration.addUrlPatterns("/*");
            return registration;
    

    沿用上述的BookControllerTest类:

    @SpringBootTest
    @AutoConfigureMockMvc
    public class BookControllerTest {
        @Autowired
        private MockMvc mockMvc;
        @MockBean
        BookService bookService;
        @Test
        public void listTest() throws Exception {
    

    这样,在测试listTest的时候,会测试API为/books(GET方法),最后会进入我们自定义的UserFilter。

    Debug截图,说明能进入UserFilter: