乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 SpringMVC概述与DispatcherServlet

    • 本章导读:Spring MVC概述与DispatcherServlet
    • Spring MVC 是什么
    • MVC 设计模式
    • 前端控制器模式
    • DispatcherServlet
    • 核心组件协作
  • 第2章 控制器与请求映射

    • 本章导读:控制器与请求映射
    • Controller
    • RestController
    • RequestMapping
    • GetMapping
    • PostMapping
    • PutMapping
    • DeleteMapping
    • PathVariable
    • RESTful
    • 请求映射原理
  • 第3章 请求参数获取与转换

    • 本章导读:请求参数获取与转换
    • RequestParam
    • RequestBody
    • RequestHeader
    • CookieValue
    • Model
    • ModelAttribute
    • 数据绑定原理
    • 数据校验
  • 第4章 响应数据与视图解析

    • 本章导读:响应数据与视图解析
    • ResponseBody
    • ResponseEntity
    • ModelAndView
    • ViewResolver
    • HttpMessageConverter
    • forward与redirect
  • 第5章 拦截器过滤器与跨域

    • 本章导读:拦截器、过滤器与跨域
    • HandlerInterceptor
    • WebMvcConfigurer
    • CrossOrigin
    • 登录验证实战
  • 第6章 文件上传与异常处理

    • 本章导读:文件上传与异常处理
    • MultipartFile
    • 文件下载
    • ExceptionHandler
    • ControllerAdvice
    • RestControllerAdvice
    • ResponseStatus
  • 第7章 高级特性与最佳实践

    • 本章导读:高级特性与最佳实践
    • SessionAttributes
    • SessionAttribute
    • RedirectAttributes
    • MockMvc测试
    • 国际化
    • 最佳实践
  • 第8章 扩展与异步机制

    • 本章导读:扩展与异步机制
    • 异步请求处理
    • 自定义参数解析器
    • 内容协商

MockMvc测试

本章聚焦 Spring MVC 无需启动 Servlet 容器的测试机制。MockMvc 模拟完整的 HTTP 请求处理链路——从 DispatcherServlet 接收请求、HandlerMapping 查找处理器、Controller 执行业务逻辑、到最终生成响应——全部在 JVM 内完成。它是单元测试与集成测试 Controller 层的标准工具,也是 985 高校学生必须掌握的工程实践能力。


定义与作用

MockMvc 是 Spring Test 模块提供的核心测试类,它构建了一个模拟的 Spring MVC 运行时环境:

  • 真实的 DispatcherServlet 被实例化并初始化
  • 真实的 Controller、拦截器、异常处理器全部参与请求处理
  • 但底层没有真正的 HTTP 网络层和 Servlet 容器(如 Tomcat)

这意味着测试运行极快(毫秒级),且可以精确验证:请求映射是否正确、参数绑定是否成功、响应状态码/内容是否符合预期、JSON 结构是否正确。

生活类比:飞行模拟器

想象飞翔科技培训飞行员:

  • 启动真实 Tomcat 测试:每次训练都开一架真飞机上天,成本高、风险大、准备时间长
  • MockMvc 测试:在地面飞行模拟器里训练。驾驶舱、仪表盘、操纵杆都是真实的(真实的 DispatcherServlet、Controller),但飞机没有真的离开地面(没有网络层和 Tomcat)。学员可以反复练习起降、应对各种故障,安全且高效

核心原理

MockMvc 测试架构

关键特征:

  1. 真实组件:Controller、Service(如果注入)、拦截器、异常处理器都是真实的 Spring Bean
  2. 模拟请求:MockHttpServletRequest 模拟 HTTP 请求,支持设置 URL、Method、Header、Body、Cookie 等
  3. 模拟响应:MockHttpServletResponse 捕获生成的响应,包括状态码、Header、Body
  4. 无网络层:请求不经过 TCP/IP、不经过 Tomcat 线程池,直接在 JVM 内方法调用

适用位置与常用属性

测试类注解

注解作用说明
@SpringBootTest加载完整 Spring 应用上下文用于集成测试,Controller、Service、Repository 全部真实
@AutoConfigureMockMvc自动配置 MockMvc 实例与 @SpringBootTest 配合使用,自动注入 MockMvc
@WebMvcTest(Controller.class)只加载 Web 层轻量级,只实例化指定的 Controller 和 MVC 基础设施,不加载 Service/Repository

MockMvc 常用 API

类/方法作用
MockMvcRequestBuilders.get("/url")构造 GET 请求
MockMvcRequestBuilders.post("/url")构造 POST 请求
MockMvcRequestBuilders.put("/url")构造 PUT 请求
MockMvcRequestBuilders.delete("/url")构造 DELETE 请求
MockMvcResultMatchers.status().isOk()断言状态码 200
MockMvcResultMatchers.status().isCreated()断言状态码 201
MockMvcResultMatchers.status().isBadRequest()断言状态码 400
MockMvcResultMatchers.content().string(...)断言响应体字符串
MockMvcResultMatchers.content().json(...)断言响应体 JSON
MockMvcResultMatchers.jsonPath("$.name", is("张三"))断言 JSON 字段值
MockMvcResultMatchers.header().string("Location", ...)断言响应头

完整示例

场景

飞翔科技员工管理系统的"新增员工"和"查询员工"接口需要自动化测试。CTO 大翔要求核心接口必须有测试覆盖,架构师白歌使用 MockMvc 编写无需启动 Tomcat 的快速测试,运维李眉将测试集成到 CI 流水线。

被测试的 Controller

package com.feixiang.web.controller;

import com.feixiang.web.entity.EmployeeForm;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.Map;

@RestController
@RequestMapping("/employees")
public class EmployeeController {

    @GetMapping("/{id}")
    public Object getEmployee(@PathVariable Long id) {
        // 模拟查询
        return Map.of(
            "id", id,
            "name", "张三",
            "department", "研发部",
            "age", 25
        );
    }

    @PostMapping
    public Object createEmployee(@Valid @RequestBody EmployeeForm form) {
        return Map.of(
            "id", 1001,
            "name", form.getName(),
            "message", "创建成功"
        );
    }
}

MockMvc 测试类

package com.feixiang.web.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.feixiang.web.entity.EmployeeForm;
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.ResultActions;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.hamcrest.Matchers.*;

@SpringBootTest
@AutoConfigureMockMvc
public class EmployeeControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void testGetEmployee() throws Exception {
        mockMvc.perform(get("/employees/1")
                .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.name").value("张三"))
            .andExpect(jsonPath("$.department").value("研发部"))
            .andExpect(jsonPath("$.age").value(25));
    }

    @Test
    void testCreateEmployeeSuccess() throws Exception {
        EmployeeForm form = new EmployeeForm();
        form.setName("李四");
        form.setAge(30);
        form.setEmail("lisi@feixiang.com");
        form.setPhone("13800138001");

        String json = objectMapper.writeValueAsString(form);

        mockMvc.perform(post("/employees")
                .contentType(MediaType.APPLICATION_JSON)
                .content(json))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(1001))
            .andExpect(jsonPath("$.name").value("李四"))
            .andExpect(jsonPath("$.message").value("创建成功"));
    }

    @Test
    void testCreateEmployeeValidationFail() throws Exception {
        EmployeeForm form = new EmployeeForm();
        form.setName("");      // @NotBlank 会失败
        form.setAge(16);       // @Min(18) 会失败
        form.setEmail("bad");  // @Email 会失败

        String json = objectMapper.writeValueAsString(form);

        mockMvc.perform(post("/employees")
                .contentType(MediaType.APPLICATION_JSON)
                .content(json))
            .andExpect(status().isBadRequest());  // 400
    }
}

GET 测试详解

@Test
void testGetEmployeeWithHeaders() throws Exception {
    mockMvc.perform(get("/employees/1")
            .header("X-Request-Id", "req-2024001")
            .param("detail", "full")
            .accept(MediaType.APPLICATION_JSON))
        .andDo(result -> {
            // 打印请求和响应详情,用于调试
            System.out.println("Response: " + result.getResponse().getContentAsString());
        })
        .andExpect(status().isOk())
        .andExpect(header().exists("Content-Type"))
        .andExpect(jsonPath("$", hasKey("id")))
        .andExpect(jsonPath("$", hasKey("name")))
        .andExpect(jsonPath("$.age", greaterThanOrEqualTo(18)));
}

POST 测试详解(含重定向验证)

@Test
void testCreateWithRedirect() throws Exception {
    // 假设 Controller 返回 redirect:/employees/{id}
    mockMvc.perform(post("/employees")
            .contentType(MediaType.APPLICATION_JSON)
            .content("{\"name\":\"王五\",\"age\":28,\"email\":\"wangwu@feixiang.com\",\"phone\":\"13900139000\"}"))
        .andExpect(status().isFound())           // 302 重定向
        .andExpect(header().string("Location", containsString("/employees/")));
}

易错场景与面试考点

误区一:@WebMvcTest 和 @SpringBootTest 混用

错误代码:

@SpringBootTest
@WebMvcTest(EmployeeController.class)  // ❌ 冲突!
@AutoConfigureMockMvc
public class EmployeeControllerTest {

问题:@WebMvcTest 已经包含了 MockMvc 的自动配置,且会切片加载上下文;与 @SpringBootTest 同时使用时,Spring Test 上下文加载会冲突或行为未定义。

纠正:二选一:

  • 需要测试完整链路(含 Service、Repository)→ 用 @SpringBootTest + @AutoConfigureMockMvc
  • 只测试 Web 层(Controller 隔离测试)→ 用 @WebMvcTest

误区二:忘记设置 contentType

错误代码:

mockMvc.perform(post("/employees")
    .content(json))  // ❌ 没有设置 contentType

问题:Spring MVC 的 @RequestBody 依赖 Content-Type 头来选择 HttpMessageConverter。如果不设置 contentType(MediaType.APPLICATION_JSON),Spring MVC 可能无法正确解析 JSON,导致参数绑定失败或返回 415 Unsupported Media Type。

纠正:

mockMvc.perform(post("/employees")
    .contentType(MediaType.APPLICATION_JSON)  // ✅ 必须设置
    .content(json))

误区三:jsonPath 断言失败时信息不清晰

问题:jsonPath("$.name").value("张三") 失败时,错误信息可能难以阅读。

最佳实践:使用 Hamcrest 匹配器增强可读性:

.andExpect(jsonPath("$.name", is("张三")))
.andExpect(jsonPath("$.age", greaterThan(18)))
.andExpect(jsonPath("$.department", containsString("研发")))
.andExpect(jsonPath("$.tags", hasSize(3)))

面试高频:MockMvc 是单元测试还是集成测试?

标准回答:MockMvc 本身是一个集成测试工具,因为它加载了真实的 Spring MVC 组件(DispatcherServlet、HandlerMapping、Controller)。但它比完整的端到端测试更轻量,因为不涉及真实的 Servlet 容器和网络层。按照测试金字塔:

  • 单元测试:只测 Controller 方法本身,Mock 掉 Service(使用 Mockito)
  • MockMvc 测试:测 Controller + Spring MVC 基础设施,Service 可以是真实的或 Mock 的
  • 端到端测试:启动真实 Tomcat,通过 HTTP 客户端调用

MockMvc 位于单元测试和端到端测试之间,常被称为切片测试或轻量级集成测试。

面试高频:@WebMvcTest 和 @SpringBootTest 的区别

维度@WebMvcTest@SpringBootTest
加载范围只加载 MVC 层(Controller、Filter、WebMvcConfigurer)加载完整 Spring 上下文
Service/Repository❌ 不加载✅ 加载
MockMvc✅ 自动可用需要加 @AutoConfigureMockMvc
启动速度快慢
适用场景Controller 隔离测试端到端 Web 测试

小结

MockMvc 是 Spring Test 提供的模拟 MVC 运行时测试工具。它在不启动 Servlet 容器的情况下,通过真实的 DispatcherServlet 处理模拟的 HTTP 请求,验证 Controller 的请求映射、参数绑定、响应生成是否符合预期。配合 jsonPath 断言可以精确校验 JSON 响应结构,是 Spring MVC 项目自动化测试的核心工具。

本章与全局的关系:本章讲解了 Spring MVC 层的测试方法。MockMvc 可以测试本教程中讲解的所有注解和机制——@RequestMapping、参数绑定、数据校验、RedirectAttributes、异常处理等。掌握 MockMvc 是保障 Spring MVC 项目质量的关键工程能力。

上一页
RedirectAttributes
下一页
国际化