> 技术文档 > 如何写好单元测试:Mock 脱离数据库,告别 @SpringBootTest 的重型启动_springboot-test编写mock测试用例时,不连接数据库

如何写好单元测试:Mock 脱离数据库,告别 @SpringBootTest 的重型启动_springboot-test编写mock测试用例时,不连接数据库


如何写好单元测试:Mock 脱离数据库,告别 @SpringBootTest 的重型启动

作者:Killian(重庆) — 欢迎各位架构猎头、技术布道者联系我,项目实战丰富,代码稳健,Mock测试爱好者。
技术栈:Java 17、JUnit 5、Mockito 5、Spring Boot 3.x(可选)


一、前言

你是否遇到过以下问题:

  • 每次跑测试都要加载整个 Spring 容器,慢如蜗牛?
  • 明明只测一个方法,却启动了 Redis、MySQL、MQ 等服务?
  • 想 Mock 一个 Bean 却被 @Autowired 绑死?

这时候,我们该说:不需要 @SpringBootTest!

本篇文章将系统讲解:

  • 如何编写真正的“单元”测试(Unit Test)
  • 如何使用 Mockito 精准 Mock 依赖,避免启动数据库等外部依赖
  • 如何写出高覆盖率、快反馈、可维护的业务逻辑测试

二、为什么要避免 @SpringBootTest?

问题 描述 启动慢 @SpringBootTest 会加载整个上下文(Controller、Service、Repository、Config) 依赖重 需要配置数据库、缓存、RabbitMQ 等外部环境 不稳定 环境不一致容易导致测试 flaky(有时通过,有时失败) 非单元测试 实际上是“集成测试”,容易误用

三、正确的方式:使用 Mockito + JUnit 写真正的单元测试

示例背景

我们有一个服务类:

@Servicepublic class OrderService { private final OrderRepository orderRepository; private final PaymentClient paymentClient; public OrderService(OrderRepository orderRepository, PaymentClient paymentClient) { this.orderRepository = orderRepository; this.paymentClient = paymentClient; } public String pay(String orderId) { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new RuntimeException(\"订单不存在\")); if (order.isPaid()) { return \"重复支付\"; } boolean result = paymentClient.callPayGateway(order); if (result) { order.markPaid(); orderRepository.save(order); return \"支付成功\"; } else { return \"支付失败\"; } }}

单元测试写法(脱离容器 + Mock 依赖)

@ExtendWith(MockitoExtension.class)class OrderServiceTest { @Mock OrderRepository orderRepository; @Mock PaymentClient paymentClient; @InjectMocks OrderService orderService; @Test @DisplayName(\"支付成功时,订单状态应更新并保存\") void testPaySuccess() { Order mockOrder = new Order(\"123\", false); when(orderRepository.findById(\"123\")).thenReturn(Optional.of(mockOrder)); when(paymentClient.callPayGateway(mockOrder)).thenReturn(true); String result = orderService.pay(\"123\"); assertEquals(\"支付成功\", result); assertTrue(mockOrder.isPaid()); verify(orderRepository).save(mockOrder); } @Test @DisplayName(\"找不到订单时,应抛出异常\") void testOrderNotFound() { when(orderRepository.findById(\"999\")).thenReturn(Optional.empty()); assertThrows(RuntimeException.class, () -> orderService.pay(\"999\")); } @Test @DisplayName(\"已支付订单不应重复支付,也不应保存\") void testAlreadyPaid() { Order paidOrder = new Order(\"456\", true); when(orderRepository.findById(\"456\")).thenReturn(Optional.of(paidOrder)); String result = orderService.pay(\"456\"); assertEquals(\"重复支付\", result); verify(orderRepository, never()).save(any()); }}

四、关键技巧:Mock 什么?怎么 Mock?

1. 只 Mock “外部依赖”

  • 数据库 Repository
  • 第三方客户端(如 FeignClient、HttpClient)
  • Redis 操作、MQ 发送器、ES 操作器

2. 不 Mock 的部分

  • 自己写的业务逻辑类(即你要测的类)

3. 使用 Mockito 提供的能力

  • when(...).thenReturn(...):设置返回值
  • verify(...):验证方法是否调用
  • argThat(...):匹配参数条件
  • doThrow(...):模拟异常

五、单元测试 vs 集成测试:职责边界与框架选择

对比表格

维度 单元测试(Unit Test) 集成测试(Integration Test) 启动方式 不启动 Spring 容器 启动 Spring 容器(或部分) 测试目标 业务逻辑、算法正确性 Bean 交互、配置、环境集成 Mock 使用 必须 Mock 外部依赖 通常不 Mock,使用真实组件 性能 快,毫秒级 慢,秒级 数据源 无数据库或 H2 Mock 真正连接数据库(如 Docker 启动 MySQL) 断言粒度 精确控制方法行为 更偏向流程通路与集成稳定性

@DataJpaTest

用于测试 JPA Repository 层(不加载 Service、Controller):

@DataJpaTestclass UserRepositoryTest { @Autowired UserRepository repo; @Test @DisplayName(\"根据用户名查询用户,应返回结果\") void testFindByUsername() { User u = new User(\"tom\", \"123\"); repo.save(u); assertTrue(repo.findByUsername(\"tom\").isPresent()); }}

自动配置内嵌数据库(如 H2),速度适中,适合数据层测试。


@Mapper + MyBatis 的 Mapper 层测试(两种方式)

✅ 方式一:真实数据库 + @MybatisTest
@MybatisTest@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // 保持使用真实数据库配置class OrderMapperTest { @Autowired OrderMapper orderMapper; @Test @DisplayName(\"根据订单ID查询,应返回订单信息\") void testSelectById() { Order order = orderMapper.selectById(\"order123\"); assertNotNull(order); }}

说明:

  • @MybatisTest 会只加载 MyBatis 相关的配置(不会加载 Service、Controller)
  • 默认使用 H2,可通过 @AutoConfigureTestDatabase 强制保留 MySQL 等真实库
  • 可以测试 XML 映射、注解 SQL、分页插件等
✅ 方式二:Mock Mapper(更适合单元测试)
@ExtendWith(MockitoExtension.class)class OrderServiceTest { @Mock OrderMapper orderMapper; @InjectMocks OrderService orderService; @Test @DisplayName(\"Mock Mapper 查询订单,应返回正确订单\") void testOrderFetch() { Order mockOrder = new Order(\"order789\", false); when(orderMapper.selectById(\"order789\")).thenReturn(mockOrder); Order result = orderService.getOrder(\"order789\"); assertEquals(\"order789\", result.getId()); }}

说明:

  • Mapper 在 Service 中作为依赖,Mock 掉即可测试业务逻辑
  • 不需要数据库、不用 @SpringBootTest,速度快、适合 CI

@WebMvcTest

用于测试 Controller 层(不加载业务逻辑):

@WebMvcTest(UserController.class)class UserControllerTest { @Autowired MockMvc mockMvc; @MockBean UserService userService; @Test @DisplayName(\"调用 /hello 接口,应返回 hello tom\") void testHelloApi() throws Exception { when(userService.getName()).thenReturn(\"tom\"); mockMvc.perform(get(\"/hello\"))  .andExpect(status().isOk())  .andExpect(content().string(\"hello tom\")); }}

优点是启动快,只加载 Web 层相关 Bean,可精准控制 Controller 输入输出。


要点 说明 不使用 @SpringBootTest 减少启动时间,提高测试速度 用 @ExtendWith(MockitoExtension.class) 使用 Mockito 管理依赖注入 用 @InjectMocks 注入被测类(业务类) 用 @Mock 模拟依赖(Repository、外部接口) 每个测试只验证一件事 保证测试原子性和可维护性

六、扩展阅读

  • Mockito 官方文档:https://site.mockito.org
  • JUnit 5 用户指南:https://junit.org/junit5/docs/current/user-guide/
  • 推荐阅读:Martin Fowler《Unit Test vs Integration Test》

七、结语

如果你写单元测试还依赖 @SpringBootTest,那就像每次微波炉加热都要重启电厂。Mock 依赖、聚焦业务、轻量高效,才是测试真正的姿势。

下一次写测试时,请问自己:“我是在测试业务逻辑,还是在启动一个服务器?”

本文由 @killian 原创,转载请注明出处。
☕ 请作者喝杯咖啡,持续更新更深入的干货

💡 彩蛋时间:如果你看到了这里,说明你是那种喜欢动手实战的人。那我悄悄分享一个开发圈流传的工具试用入口,貌似跟高效调试很有关系,地址也挺特别的:

🔗 入口

据说注册还能解锁一些隐藏功能,懂的都懂(别外传 😂)

重庆票务预订