avatar

Ryan's Blog

The first step is always the hardest.

  • 首页
  • 分类
  • 标签
  • 归档
  • 关于
  • 工具
Home Spring Boot 单元测试最佳实践:JUnit 5、Mockito、MockMvc 与测试切片
文章

Spring Boot 单元测试最佳实践:JUnit 5、Mockito、MockMvc 与测试切片

Posted 2023-04-8 Updated 3 days ago
By Ryan Chen
29~38 min read

测试用例的价值不只是“提高覆盖率”,更重要的是让代码在持续迭代中保持可验证、可重构、可回归。Spring Boot 提供了完整的测试支持,但很多项目会把所有测试都写成 @SpringBootTest,导致启动慢、依赖重、数据互相污染,最终测试越来越难维护。

这篇文章整理 Spring Boot 测试的常用实践,重点区分单元测试、切片测试和集成测试,并说明 JUnit 5、Mockito、MockMvc、WebTestClient、Testcontainers 等工具的使用边界。

单元测试、切片测试、集成测试的区别

写测试前,先明确测试范围。

类型 目标 是否启动 Spring 容器 典型工具
单元测试 验证单个类或方法逻辑 否 JUnit 5、Mockito
切片测试 验证某一层组件 部分启动 @WebMvcTest、@DataJpaTest、@JsonTest
集成测试 验证完整应用上下文和外部依赖 是 @SpringBootTest、Testcontainers

经验建议:

  • 能用普通 JUnit + Mockito 解决的,不要上 @SpringBootTest。
  • Controller 层优先用 @WebMvcTest。
  • Repository 层优先用 @DataJpaTest。
  • 需要完整链路时再使用 @SpringBootTest。

spring-boot-starter-test 包含什么

通常只需要引入:

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

它会集成 Spring Boot 测试常用能力,例如:

  • JUnit Jupiter
  • Spring Test / Spring Boot Test
  • AssertJ
  • Hamcrest
  • Mockito
  • JSONassert
  • JsonPath

在较新的 Spring Boot 项目中,应优先使用 JUnit 5,而不是旧的 JUnit 4 @RunWith(SpringRunner.class) 写法。

JUnit 5 基础用法

JUnit 5 常用注解:

注解 说明
@Test 测试方法
@BeforeEach 每个测试前执行
@AfterEach 每个测试后执行
@BeforeAll 所有测试前执行
@AfterAll 所有测试后执行
@DisplayName 自定义测试名称
@ParameterizedTest 参数化测试

示例:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class PriceCalculatorTest {

    @Test
    @DisplayName("会员价格应享受 8 折优惠")
    void shouldCalculateMemberPrice() {
        PriceCalculator calculator = new PriceCalculator();

        int result = calculator.memberPrice(100);

        assertThat(result).isEqualTo(80);
    }
}

建议使用 AssertJ 的链式断言,表达力更强。

Mockito 单元测试

当被测类依赖其他组件时,可以使用 Mockito 隔离外部依赖。

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void shouldReturnUserName() {
        when(userRepository.findNameById(1L)).thenReturn("Ryan");

        String name = userService.getUserName(1L);

        assertThat(name).isEqualTo("Ryan");
    }
}

注意:

  • @Mock 是 Mockito 的 Mock,不需要 Spring 容器。
  • @InjectMocks 会把 Mock 注入到被测对象。
  • 这种测试启动最快,适合业务逻辑类。

Spring Boot 测试切片

测试切片的目标是只启动某一层所需的 Spring 组件,降低上下文启动成本。

@WebMvcTest

适合测试 Controller 层。

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.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void shouldGetUser() throws Exception {
        mockMvc.perform(get("/users/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(0));
    }
}

如果 Controller 依赖 Service,需要提供 Mock Bean。Spring Boot 3.4 起 @MockBean 已被标记为 deprecated,后续应关注 @MockitoBean 等替代方案;如果项目仍在 Spring Boot 2.x / 3.3 及以前版本,可以继续按当前版本文档使用 @MockBean。

@DataJpaTest

适合测试 JPA Repository 层。

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldSaveUser() {
        User user = new User("Ryan");
        userRepository.save(user);

        assertThat(userRepository.findByName("Ryan")).isPresent();
    }
}

默认情况下,@DataJpaTest 通常会在事务中运行测试,并在测试后回滚,适合保持数据隔离。

@JsonTest

适合测试 JSON 序列化和反序列化。

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;

import static org.assertj.core.api.Assertions.assertThat;

@JsonTest
class UserJsonTest {

    @Autowired
    private JacksonTester<UserDTO> json;

    @Test
    void shouldSerializeUser() throws Exception {
        UserDTO user = new UserDTO(1L, "Ryan");

        assertThat(json.write(user)).hasJsonPathStringValue("$.name");
        assertThat(json.write(user)).extractingJsonPathStringValue("$.name").isEqualTo("Ryan");
    }
}

@SpringBootTest 集成测试

@SpringBootTest 会启动完整 Spring 应用上下文,适合验证完整链路。

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

@SpringBootTest
@ActiveProfiles("test")
class ApplicationContextTest {

    @Test
    void contextLoads() {
    }
}

如果需要启动真实 Web 服务,可以使用随机端口:

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ServerTest {

    @LocalServerPort
    int port;

    @Test
    void shouldStartServer() {
        assertThat(port).isGreaterThan(0);
    }
}

常见 webEnvironment:

类型 说明
MOCK Mock Web 环境,不启动真实端口
RANDOM_PORT 启动真实服务,使用随机端口
DEFINED_PORT 使用配置中的固定端口
NONE 非 Web 应用上下文

MockMvc 与 WebTestClient

MockMvc

适合 Servlet / Spring MVC 应用。

mockMvc.perform(get("/users/1"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.data.id").value(1));

WebTestClient

适合 WebFlux,也可以用于部分集成测试场景。

webTestClient.get()
        .uri("/users/1")
        .exchange()
        .expectStatus().isOk()
        .expectBody()
        .jsonPath("$.data.id").isEqualTo(1);

选择建议:

  • Spring MVC:优先 MockMvc。
  • Spring WebFlux:优先 WebTestClient。
  • 真实 HTTP 端到端测试:可使用 TestRestTemplate、WebTestClient 或 RestClient 等。

JSONAssert 与 JsonPath

复杂 JSON 响应可以用 JSONAssert 或 JsonPath。

JSONAssert:

JSONAssert.assertEquals(
        "{\"code\":0,\"msg\":\"success\"}",
        actualJson,
        JSONCompareMode.LENIENT
);

JsonPath:

Integer code = JsonPath.read(actualJson, "$.code");
assertThat(code).isZero();

实践建议:

  • 不要对整段 JSON 做过度严格匹配。
  • 只断言业务关键字段。
  • 对列表结果要关注顺序是否有业务意义。

Testcontainers

如果测试依赖 MySQL、Redis、Kafka 等外部组件,使用本地共享环境容易导致测试不稳定。Testcontainers 可以在测试期间启动真实容器。

示例:

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
@SpringBootTest
class UserIntegrationTest {

    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");

    @Test
    void shouldRunWithRealMysql() {
        // integration test
    }
}

Testcontainers 更适合集成测试,不建议把所有单元测试都容器化,否则测试会变慢。

降低测试耦合的实践

  1. 按测试层级选择工具

    • 业务逻辑:JUnit + Mockito。
    • Controller:@WebMvcTest。
    • Repository:@DataJpaTest。
    • 完整链路:@SpringBootTest。
  2. 避免测试之间共享可变数据

    • 每个测试自己准备数据。
    • 使用事务回滚或清理脚本。
    • 不依赖测试执行顺序。
  3. 不要过度 Mock

    • Mock 太多会让测试只验证 Mock 行为,而不是业务行为。
    • 核心领域逻辑尽量设计成纯 Java 类,减少框架依赖。
  4. 测试命名表达业务语义

    • 推荐:shouldReturnUserWhenUserExists。
    • 避免:test1、testGet。
  5. 控制 Spring 上下文数量

    • 不要随意组合大量不同配置,避免测试缓存失效导致整体变慢。

常见问题

1. 为什么测试很慢

常见原因:

  • 大量使用 @SpringBootTest。
  • 每个测试类都启动不同 Spring Context。
  • 依赖外部数据库或中间件。
  • 测试数据准备过重。

优化:

  • 优先使用单元测试和切片测试。
  • 复用测试配置。
  • 使用 Testcontainers 时按需启用。

2. @Mock 和 @MockBean 有什么区别

  • @Mock:Mockito 提供,不进入 Spring 容器。
  • @MockBean:Spring Boot 测试提供,会把 Mock 对象注册进 Spring 容器,替换原 Bean。

新版本中要关注 @MockBean 的替代方案和当前项目 Spring Boot 版本的官方文档。

3. 测试是否应该连接真实数据库

取决于测试目标:

  • 单元测试:不需要。
  • Repository 测试:可以使用内存数据库或 Testcontainers。
  • 集成测试:建议使用尽可能接近生产的真实依赖,但要做好隔离。

总结

Spring Boot 测试的核心原则是:用最小成本验证最关键行为。

  • 不要把所有测试都写成 @SpringBootTest。
  • 单元测试优先 JUnit 5 + Mockito。
  • Web 层优先 @WebMvcTest + MockMvc。
  • 数据层优先 @DataJpaTest。
  • 外部依赖集成测试可使用 Testcontainers。
  • 测试数据要隔离,测试命名要表达业务语义。

这样测试才能长期可维护,而不是随着项目复杂度上升逐渐失效。

参考资料

  • Spring Boot 官方文档:Testing
  • Spring Boot 官方文档:Testing Spring Boot Applications
  • JUnit 5 官方文档
  • Mockito 官方文档
  • Testcontainers 官方文档
Java
Spring Boot 单元测试 JUnit 5 Mockito MockMvc Testcontainers 测试实践 Java
License:  CC BY 4.0
Share

Further Reading

Sep 12, 2024

RocketMQ 架构设计与应用最佳实践:高可用消息队列核心解析

本文基于 RocketMQ 4.x 经典架构,梳理 NameServer、Broker、Producer、Consumer、Remoting 与 Store 模块,结合消息轨迹、存储模型、FastFail、事务消息和高可用部署,总结高并发场景下的实践要点。

Aug 16, 2023

Java List 核心数据结构解析:ArrayList、LinkedList 与线程安全

系统梳理 Java List 接口、ArrayList 动态数组、LinkedList 双向链表、容量扩容、遍历与 fail-fast 机制,并对比 synchronizedList、CopyOnWriteArrayList、Vector 等线程安全方案的适用场景。

Apr 24, 2023

数组基础详解:概念、存储结构与常用操作

从数组的连续存储、下标访问、Java 数组对象、一维与多维数组、遍历、查找、插入、删除、复制、排序和 Arrays 工具类出发,系统梳理数据结构学习中的数组基础。

OLDER

Docker Engine 与 Docker Compose V2 安装配置指南

NEWER

APISIX Dashboard 使用指南:路由、服务、上游与插件配置实践

Recently Updated

  • Agent 架构设计原则:Router、Runtime 与 Business Script 的职责划分
  • RocketMQ 架构设计与应用最佳实践:高可用消息队列核心解析
  • Redis 核心概念、数据结构与高可用架构详解
  • B+树原理与 MySQL InnoDB 索引机制解析
  • MySQL AUTO_INCREMENT 插入 0 变成自增值的原因与解决方案

Trending Tags

RocketMQ Windows Feign Docker Zipkin SonarQube OkHttp HttpClient API 性能优化

Contents

©2026 Ryan's Blog. Some rights reserved. · 粤ICP备2022031588号