源本科技 | 码上会

Spring Boot MockMVC

2026/04/01
2
0

引言

Spring Boot MockMVC 是 Spring 官方提供的 Web 层专用测试框架,无需启动真实的 Web 服务器,即可对 Spring Boot 应用的 REST 控制器进行测试。它能模拟 GET、POST、PUT、DELETE 等 HTTP 请求,并验证 JSON 响应、状态码、响应头等核心信息。


核心特性

  • 无需启动服务器,轻量级测试 REST 接口

  • 模拟完整 HTTP 请求 / 响应流程

  • 支持校验状态码、JSON 字段、响应头

  • 结合 Lombok 简化代码,告别冗余 Getter/Setter

  • 执行速度快,专注 Web 层接口测试

核心方法

方法

作用

perform()

执行 HTTP 请求

andExpect()

响应结果断言

status().isOk()

校验状态码 200

jsonPath()

校验 JSON 字段

andDo(print())

打印请求 / 响应日志


技术栈

Spring Boot 3.x + JDK 17 + JUnit 5 + Mockito + MockMVC + Lombok


项目实现

项目结构

com.example.demo
├── domain          # 实体类
│   └── Employee.java
├── util            # 工具类
│   └── EmployeeIdGenerator.java
├── service         # 业务层
│   └── EmployeeService.java
├── controller      # REST 控制器
│   └── EmployeeController.java
└── DemoApplication.java  # 启动类

步骤1:Maven 依赖

JDK17 + Lombok + Web + 测试依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>mockmvc-rest-lombok</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- Web 核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Lombok 简化代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 测试依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

步骤2:实体类

package com.example.demo.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 员工实体类(Lombok 简化)
 */
@Data // 自动生成 Getter/Setter/toString/equals/hashCode
@NoArgsConstructor // 无参构造
@AllArgsConstructor // 全参构造
public class Employee {
    private Long employeeId;
    private String firstName;
    private String lastName;
    private int salary;
}

步骤3:ID 生成工具类

package com.example.demo.util;

public class EmployeeIdGenerator {
    private static long employeeId = 1000;

    public static synchronized long value() {
        return employeeId++;
    }
}

步骤4:业务层

package com.example.demo.service;

import com.example.demo.domain.Employee;
import com.example.demo.util.EmployeeIdGenerator;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@Service
public class EmployeeService {
    private final Map<Long, Employee> employeeMap = new HashMap<>();

    // 查询所有
    public Collection<Employee> findAll() {
        return employeeMap.values();
    }

    // 根据ID查询
    public Optional<Employee> findById(Long employeeId) {
        return Optional.ofNullable(employeeMap.get(employeeId));
    }

    // 新增
    public Employee save(Employee employee) {
        long id = EmployeeIdGenerator.value();
        employee.setEmployeeId(id);
        employeeMap.put(id, employee);
        return employee;
    }

    // 更新
    public Optional<Employee> update(Employee employee) {
        if (!employeeMap.containsKey(employee.getEmployeeId())) {
            return Optional.empty();
        }
        employeeMap.put(employee.getEmployeeId(), employee);
        return Optional.of(employee);
    }

    // 删除
    public Optional<Employee> delete(Long employeeId) {
        return Optional.ofNullable(employeeMap.remove(employeeId));
    }
}

步骤5:Controller

package com.example.demo.controller;

import com.example.demo.domain.Employee;
import com.example.demo.service.EmployeeService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.net.URI;
import java.util.Collection;

@RestController
@RequestMapping("/employees")
public class EmployeeController {
    private final EmployeeService employeeService;

    // 构造器注入
    public EmployeeController(EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

    // 查询所有
    @GetMapping
    public Collection<Employee> getAll() {
        return employeeService.findAll();
    }

    // 根据ID查询
    @GetMapping("/{id}")
    public Employee getById(@PathVariable Long id) {
        return employeeService.findById(id)
                .orElseThrow(EmployeeNotFoundException::new);
    }

    // 新增
    @PostMapping
    public ResponseEntity<Employee> create(@RequestBody Employee employee) {
        Employee saved = employeeService.save(employee);
        URI uri = ServletUriComponentsBuilder
                .fromCurrentRequest()
                .path("/{id}")
                .buildAndExpand(saved.getEmployeeId())
                .toUri();
        return ResponseEntity.created(uri).body(saved);
    }

    // 更新
    @PutMapping
    public Employee update(@RequestBody Employee employee) {
        return employeeService.update(employee)
                .orElseThrow(EmployeeNotFoundException::new);
    }

    // 删除
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        employeeService.delete(id)
                .orElseThrow(EmployeeNotFoundException::new);
    }

    // 自定义异常
    @ResponseStatus(HttpStatus.NOT_FOUND)
    static class EmployeeNotFoundException extends RuntimeException {
        public EmployeeNotFoundException() {
            super("员工不存在");
        }
    }
}

步骤6:启动类

package com.example.demo;

import com.example.demo.domain.Employee;
import com.example.demo.service.EmployeeService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    // 初始化数据
    @Bean
    CommandLineRunner initData(EmployeeService service) {
        return args -> {
            service.save(new Employee(null, "Rachel", "Green", 100000));
            service.save(new Employee(null, "Monica", "Geller", 40000));
        };
    }
}

MockMVC 测试

package com.example.demo.controller;

import com.example.demo.domain.Employee;
import com.example.demo.service.EmployeeService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import java.util.Optional;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(EmployeeController.class)
class EmployeeControllerTest {
    @Autowired
    MockMvc mockMvc;

    @MockBean
    EmployeeService employeeService;

    @Autowired
    ObjectMapper objectMapper;

    // 1. 查询所有员工
    @Test
    void getAllEmployeesTest() throws Exception {
        List<Employee> list = List.of(
                new Employee(1000L, "Rachel", "Green", 100000),
                new Employee(1001L, "Monica", "Geller", 40000)
        );
        when(employeeService.findAll()).thenReturn(list);

        mockMvc.perform(get("/employees")
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()").value(2))
                .andDo(print());
    }

    // 2. 根据ID查询
    @Test
    void getEmployeeByIdTest() throws Exception {
        Employee emp = new Employee(1000L, "Rachel", "Green", 100000);
        when(employeeService.findById(1000L)).thenReturn(Optional.of(emp));

        mockMvc.perform(get("/employees/1000"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.employeeId").value(1000))
                .andExpect(jsonPath("$.firstName").value("Rachel"))
                .andDo(print());
    }

    // 3. 员工不存在 404
    @Test
    void employeeNotFoundTest() throws Exception {
        when(employeeService.findById(9999L)).thenReturn(Optional.empty());

        mockMvc.perform(get("/employees/9999"))
                .andExpect(status().isNotFound());
    }

    // 4. 新增员工
    @Test
    void createEmployeeTest() throws Exception {
        Employee emp = new Employee(null, "Test", "User", 50000);
        String json = objectMapper.writeValueAsString(emp);

        mockMvc.perform(post("/employees")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(status().isCreated())
                .andDo(print());
    }

    // 5. 删除员工
    @Test
    void deleteEmployeeTest() throws Exception {
        when(employeeService.delete(1000L)).thenReturn(Optional.of(new Employee()));

        mockMvc.perform(delete("/employees/1000"))
                .andExpect(status().isNoContent());
    }
}