从类结构理解MockMVC

464 阅读8分钟

看了很多讲MockMvc的文章,基本都是都是直接讲使用过程,头绪太多太杂乱,很难理解和记忆。本文从MockMvc的类结构入手,试着将MockMvc的处理逻辑捋清楚。

1. 什么是MockMvc

MockMvc 是 Spring Test 框架的一部分,它用于测试 Spring MVC 控制器,而无需启动完整的 HTTP 服务器。

可以使用 MockMvc 来发送模拟 HTTP 请求到控制器,并验证控制器的响应和模型。这使得可以在不部署应用程序或连接到数据库的情况下进行全面的测试。

官方描述:MockMvc从 spring-test 模块调用 DispatcherServlet ,并传递 Servlet API 的“模拟”实现,在没有运行服务器的情况下复制完整的 Spring MVC 请求处理。

使用MockMvc在Spring中进行测试时,虽然测试了控制器(C)、视图(V)和模型(M)的交互,从原理划分上更接近集成测试,但是由于它们不需要部署应用程序或连接到数据库,因此通常仍然被归类为单元测试。

2. MockMvc的处理流程

MockMvc 从测试执行时收到请求,到收到响应的处理流程如下图所示:

image.png

标号说明
1️⃣测试方法(TestCase)在Spring Test提供的TestDispatcherServlet中,设置要请求的数据。
2️⃣MockMvc 向 TestDispatcherServlet 发送伪请求。
3️⃣TestDispatcherServlet 调用 Controller 中的一个方法,来匹配请求的详细信息
4️⃣测试方法(TestCase)从MockMvc接收执行结果,并验证执行结果的有效性

3. MockMvc 和 MockServer 的区别

MockMvcMockServer 都是用于测试的工具,但它们的使用场景和目标有所不同。MockMvc 主要用于测试 Spring 控制器,而 MockServer 用于模拟应用程序依赖的外部服务

MockMvc 是 Spring Test 框架提供的一个工具,主要用于测试 Spring MVC 和 Spring REST 控制器。它允许发送 HTTP 请求到控制器,并验证控制器的响应。这些测试在一个模拟的 Servlet 容器环境中运行,不需要实际的服务器。

MockMvc的示例代码如下:

MockMvc MockMvc = MockMvcBuilders.standaloneSetup(new HelloController()).build();
MockMvc.perform(get("/hello")).andExpect(status().isOk());

MockServer 是一个更通用的工具,可以模拟任何 HTTP 服务器,包括 RESTful 服务、SOAP 服务等。它可以用来模拟应用程序依赖的外部 HTTP 服务,以便可以在没有实际服务的情况下测试应用程序。

MockServer的示例代码如下:

MockServerClient mockServer = new MockServerClient("localhost", 1080);
mockServer.when(request().withPath("/hello")).respond(response().withStatusCode(200));

4. MockMvc的类结构

4.1 使用MockMvc测试的逻辑过程

以下是MockMvc测试过程中涉及到的各个类或接口,后面的章节将对各个类或接口分别描述。

  • 构造MockMvc实例:使用MockMvcBuilder构造一个MockMvc的实例 mockMvc,或在添加@MockMvcTest注解后通过自动注入创建MockMvc实例;

  • 执行测试:mockMvc 调用 perform方法,这个方法接受一个RequestBuilder对象的参数,去调用 controller 的业务处理逻辑;

  • 获得返回结果对象perform 方法返回一个实现了 ResultActions接口的对象(Controller接口的响应结果);可以在 ResultActions 对象上进行下一步的操作:如进行验证、执行一个处理、或继续将结果封装为MvcResult对象 ;

  • 对结果进行验证:ResultActions对象进行验证时,接受一个实现了ResultMatcher接口的对象作为参数,实现两种验证方式:对请求结果进行验证,或对请求返回的内容进行验证;

4.2 MockMvcBuilder

MockMvcBuilder是用来构建MockMvc实例的构造器,有下面两个方法构建方式:

  • 方法一:webAppContextSetup(WebApplicationContext context):这个方法会创建一个 “模拟整个 Spring 应用程序上下文”的MockMvc 实例。这意味着它将加载本项目所有的控制器、配置类、过滤器等,就像在实际的 Servlet 容器中一样。这种方式更接近于集成测试,因为它涉及到更多的组件交互。
  @Autowired
  private WebApplicationContext context;
  @BeforeEach
  public void setup() {
      mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
  }
  • 方法二:standaloneSetup(Object... controllers):这个方法接收一个控制器对象作为参数,用于创建一个 “只包含这个指定的控制器” 的 MockMvc 实例。这意味着它不会加载整个 Spring 上下文,只会加载指定的控制器。这种方式更接近于单元测试,因为它只关注单个控制器的行为。
  @BeforeEach
  public void setup() {
      mockMvc = MockMvcBuilders.standaloneSetup(new MyController()).build();
  }

4.3 @WebMvcTest 注解

@WebMvcTest 应用在测试类上,是 Spring Boot 提供的一个专门测试 Spring MVC 控制器的注解。它是一个组合注解,包含了 @ExtendWith(SpringExtension.class)@WebAppConfiguration 等多个其他的 Spring 测试注解。

  • @WebMvcTest 注解可以接受一个或多个控制器类作为参数,这样 Spring Boot 只会创建这些控制器的实例,而不会创建其他的控制器。

  • 如果不指定@WebMvcTest 注解的参数,则Spring Boot 会创建一个类型为 WebApplicationContext 的应用上下文,然后在这个上下文中只注册与 MVC 测试相关的 beans,即Spring Boot 将尝试加载所有的 @Controller@ControllerAdvice@JsonComponentFilterWebMvcConfigurerHandlerMethodArgumentResolver。然而它不会加载 @Component@Service@Repository 等其他类型的 beans。

  • @WebMvcTest 注解会自动配置 Spring Security

  • 添加@WebMvcTest注解后,就可以使用注入方式创建一个MockMvc实例。

如果控制器依赖于其他的服务或仓库,需要使用 @MockBean@Import 来提供这些依赖项的 mock 或实现。这是因为 @WebMvcTest 的目标是提供一个快速和轻量级的测试环境,专门用于测试 MVC 层的代码,而不是整个应用上下文。如果需要加载完整的应用上下文,则应该使用@SpringBootTest注解。

@WebMvcTest(EmployeeController.class)
public class TestEmployeeController {
  @Autowired
  private MockMvc mvc;
}

4.4 MockMvcRequestBuilders

有了MockMvc实例,就可以调用其perform方法进行测试,这个方法需要一个封装了请求信息的RequestBuilder对象来作为参数。

RequestBuilder 对象是由Spring MVC 测试框架通过MockMvcRequestBuilders 类的一系列的静态方法来创建的,用以模拟 各种HTTP 请求。

以下是 MockMvcRequestBuilders 中的一些常用的方法:

  • get(String urlTemplate, Object... uriVars):创建一个 GET 请求。

  • post(String urlTemplate, Object... uriVars):创建一个 POST 请求。

  • put(String urlTemplate, Object... uriVars):创建一个 PUT 请求。

  • delete(String urlTemplate, Object... uriVars):创建一个 DELETE 请求。

  • patch(String urlTemplate, Object... uriVars):创建一个 PATCH 请求。

  • options(String urlTemplate, Object... uriVars):创建一个 OPTIONS 请求。

  • head(String urlTemplate, Object... uriVars):创建一个 HEAD 请求。

  • fileUpload(String urlTemplate, Object... uriVars):创建用于文件上传的 POST 请求。

每个方法都接受一个 URL 模板(urlTemplate)和一系列的 URI 变量(uriVars[])作为参数。URL 模板可以包含占位符,例如 “/items/{itemId}”,URI 变量将用于替换这些占位符。

这些方法返回的 RequestBuilder 实例可以用于设置请求头、请求参数、请求体等,然后传递给 MockMvc.perform() 方法以发送测试请求。

@SpringBootTest
@AutoConfigureMockMvc
public class MyControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testCreateProduct() throws Exception {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);

        JSONObject request = new JSONObject();
        request.put("name", "Harry Potter");
        request.put("price", 450);

        RequestBuilder requestBuilder = MockMvcRequestBuilders
                .post("/products")
                .headers(httpHeaders)
                .contentType(MediaType.APPLICATION_JSON)
                .content(request.toJSONString());

        mockMvc.perform(requestBuilder)
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().string("Hello, World!"));
    }
}

4.5 ResultActions

ResultActions 是一个接口,MockMvc.perform() 方法返回的结果是一个实现了此接口的对象。

ResultActions有以下三个方法:

  • andExpect:添加执行完成后的断言。以一个实现了ResultMatcher接口的对象作为参数(包含验证规则),验证Controller返回的结果是否正确;

  • andDo:添加一个实现了ResultHandler接口的结果处理器;如 .andDo(MockMvcResultHandlers.print()),能够输出整个响应结果信息,以在调试的时候使用。

  • andReturn:将ResultActions对象继续封装为一个MvcResult对象后返回;


String example= "{"id":1, "name":"kqzu"}";  

//accept指定客户端能够接收的内容类型
mockMvc.perform(post("/user")
        .content(example)
        .accept(MediaType.APPLICATION_JSON)
    )
    //验证响应的内容类型是否是UTF-8
    .andExpect(content().contentType("application/json;charset=UTF-8")) 
    //验证id是否为1
    .andExpect(jsonPath("$.id").value(1)) 
    // 验证name  
    .andExpect(jsonPath("$.name").value("kqzhu");  

// 和example中的name不同,多了一个‘h'字符
String errorExample = "{'id':1, 'name':'kqzhu'}"; 

MvcResult result = mockMvc.perform(post("/user")  
        .content(errorExample)  
        .accept(MediaType.APPLICATION_JSON)
    )  
    //400错误请求,status().isOk() 正确  status().isNotFound() 验证控制器不存在
    .andExpect(status().isBadRequest()) 
    //返回MvcResult
    .andReturn();

4.6 ResultMatchers

ResultMatcher 是一个接口,它表示一个用于验证 controller 响应结果的匹配器。实现了ResultMatcher接口的对象,可以用作andExpect方法的参数。

Spring MVC 测试框架提供了一些 ResultMatcher 的实现类,可以使用 MockMvcResultMatchers 类的静态方法来创建这些 ResultMatcher。以下是一些常用的静态方法 :

  • status():用于验证 HTTP 状态码。例如,status().isOk() 用于验证状态码是否为 200。

  • view():用于验证返回的视图名称。例如,view().name("home") 用于验证视图名称是否为 “home”。

  • model():用于验证模型属性。例如,model().attributeExists("attributeName") 用于验证模型中是否存在名为 “attributeName” 的属性。

  • content():用于验证响应内容。例如,content().string("expectedContent") 用于验证响应内容是否为 “expectedContent”。

  • header():用于验证 HTTP 响应头。例如,header().string("Content-Type", "application/json") 用于验证 “Content-Type” 响应头是否为 “application/json”。

  • jsonPath():用于验证 JSON 响应内容。例如,jsonPath("$.id").value(1) 用于验证 JSON 对象的 “id” 属性是否为 1。

  • xpath():用于验证 XML 响应内容。例如,xpath("/User/Id").string("123") 用于验证 XML 中的 “/User/Id” 节点的值是否为 “123”。

这些 ResultMatcher 可以通过andExpect方法进行链式调用,以在一个测试方法中验证多个条件。

    @Test
    public void testMyMethod() throws Exception {
        mockMvc.perform(get("/my-endpoint"))
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andExpect(MockMvcResultMatchers.content().string("Hello, World!"))
            .andExpect(MockMvcResultMatchers.model().attributeExists("myAttribute"));
    }

4.7 MvcResult

MvcResult 封装了执行完 controller 后得到的整个结果,而不仅仅是返回值,它包含了测试结果的所有信息。

使用ResultActions对象的 andReturn() 方法,即可获得MvcResult 对象。

MvcResult 常用的方法包括:

  • MockHttpServletRequest getRequest():得到执行的请求;

  • MockHttpServletResponse getResponse():得到执行后的响应;

  • Object getHandler():得到执行的处理器,一般就是控制器;

  • HandlerInterceptor[] getInterceptors():得到对处理器进行拦截的拦截器;

  • ModelAndView getModelAndView():得到执行后的ModelAndView;

  • Exception getResolvedException():得到HandlerExceptionResolver解析后的异常;

  • FlashMap getFlashMap():得到FlashMap;

  • Object getAsyncResult() 或 Object getAsyncResult(long timeout):得到异步执行的结果;

@SpringBootTest
@AutoConfigureMockMvc
public class MyControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testMyMethod() throws Exception {
        MvcResult mvcResult = mockMvc
            .perform(MockMvcRequestBuilders.get("/my-endpoint"))
            .andReturn();

        int status = mvcResult.getResponse().getStatus();
        String content = mvcResult.getResponse().getContentAsString();

        System.out.println("HTTP Status = " + status);
        System.out.println("Response content = " + content);
    }
}

5. MockMvc使用实例

5.1 引入Maven依赖

MockMvc已经包含在Spring Test中,所以引入Spring Test 即可:

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.2.4</version>
</parent>

<dependencies>
  ...
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
  ...
</dependencies>

5.2 编写 JUnit 5 测试

现在开始使用 JUnit 5 注解编写测试类和方法。用 @SpringBootTest 加载 Spring Boot 应用程序上下文以进行集成测试。

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class MyServiceTest {
    @Autowired
    private MyService myService;
    @Test
    public void testSomeMethod() {
        // Your JUnit 5 test logic here
        // You can use assertions and other JUnit 5 features
    }
}

测试类应写入 /src/test/java/ 目录中合适的包层次结构中。Spring Boot maven 插件提供了各种与测试相关的功能。例如,它捕获并显示测试输出,包括测试结果和测试期间引发的任何异常。

5.3 运行测试

可以像使用任何 IDE 或构建工具(如 Maven)一样运行 JUnit 5 测试:

mvn test

5.4 Demo

5.4.1 创建测试用的Controller

@RestController
@RequestMapping(path = "/employees")
public class EmployeeController {
    
    @Autowired
    private EmployeeDAO employeeDao;
    
    @GetMapping(path="/", produces = "application/json")
    public Employees getEmployees() {
        return employeeDao.getAllEmployees();
    }
    
    @PostMapping(path= "/", 
        consumes = "application/json", 
        produces = "application/json")
    public ResponseEntity<Object> addEmployee(@RequestBody Employee employee) {
        employeeDao.addEmployee(employee);
        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
                            .path("/{id}")
                            .buildAndExpand(employee.getId())
                            .toUri();
        return ResponseEntity.created(location).build();
    }
}

5.4.2 创建 controller 的测试类

@WebMvcTest(EmployeeController.class)
public class TestEmployeeRESTController {
  @Autowired
  private MockMvc mvc;
}

5.4.3 编写测试方法

最后,编写测试类中的测试方法,使用 MockMvc bean 实例调用 API 并验证结果。

以下实例代码中,添加了三个期望,分别是:

  • 返回正常状态200;
  • 返回结果中,包含‘$.employees’字符串;
  • 返回的结果字符串中,包含的‘$.employees[*].employeeId’不为空;
@Test
public void getAllEmployeesAPI() throws Exception {
  mvc.perform(MockMvcRequestBuilders
                  .get("/employees")
                  .accept(MediaType.APPLICATION_JSON))
      .andDo(print())
      .andExpect(status().isOk())
      .andExpect(MockMvcResultMatchers.jsonPath("$.employees").exists())
      .andExpect(MockMvcResultMatchers.jsonPath("$.employees[*].employeeId")
                  .isNotEmpty());
}

参考

  1. MoccServer简介
  2. MockServer的使用
  3. Mockto应用指南
  4. 从类结构理解MockMVC
  5. Junit5单元测试