如何写好单元测试: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?
三、正确的方式:使用 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 集成测试:职责边界与框架选择
对比表格
@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 输入输出。
六、扩展阅读
- 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 原创,转载请注明出处。
☕ 请作者喝杯咖啡,持续更新更深入的干货
💡 彩蛋时间:如果你看到了这里,说明你是那种喜欢动手实战的人。那我悄悄分享一个开发圈流传的工具试用入口,貌似跟高效调试很有关系,地址也挺特别的:
🔗 入口
据说注册还能解锁一些隐藏功能,懂的都懂(别外传 😂)