JUnit 5单元测试(三)—— Mockito 模拟实战:从零构建隔离测试环境
1. 为什么需要Mockito隔离测试环境第一次接触单元测试时我遇到过这样的尴尬场景测试一个订单支付功能每次运行测试都要真实调用支付宝接口不仅测试速度慢还因为网络波动经常失败。更糟的是有次测试数据污染了生产环境差点引发线上事故。这时候我才真正理解Mockito的价值——它能让测试像在无菌实验室里进行完全掌控所有变量。现代系统常见的三大测试痛点Mockito都能完美解决外部依赖不可控比如数据库查询可能超时、第三方接口可能限流。通过Mockito模拟的DAO层对象可以立即返回预设数据不再受网络或服务状态影响。我做过对比测试真实数据库查询平均需要200ms而Mock对象仅需2ms。测试数据污染曾经有个同事在测试中误删了用户表数据。用Mock技术后所有数据库操作实际只在内存中模拟根本不会触及真实数据存储。复杂场景难以构造比如测试支付失败重试逻辑真实环境很难让支付网关连续报错。用Mockito的thenThrow()可以轻松模拟连续异常// 模拟支付服务连续3次超时 when(paymentService.process(any())) .thenThrow(new TimeoutException()) .thenThrow(new TimeoutException()) .thenReturn(success);实际项目中这些场景特别适合用Mockito微服务间的Feign/RestTemplate调用MyBatis/JPA数据库操作Redis缓存读写Kafka/RabbitMQ消息生产消费外部SDK如短信、OSS存储2. 快速搭建Mockito测试环境2.1 依赖配置的坑与技巧最近Mockito 5.x开始要求JDK11多数项目还在用JDK8这里推荐4.x最终版4.11.0。除了核心库有两个扩展包特别实用!-- 基础mock功能 -- dependency groupIdorg.mockito/groupId artifactIdmockito-core/artifactId version4.11.0/version scopetest/scope /dependency !-- 支持JUnit5注解 -- dependency groupIdorg.mockito/groupId artifactIdmockito-junit-jupiter/artifactId version4.11.0/version scopetest/scope /dependency !-- 静态方法mock谨慎使用 -- dependency groupIdorg.mockito/groupId artifactIdmockito-inline/artifactId version4.11.0/version scopetest/scope /dependency踩坑提醒静态方法mock需要mockito-inline但可能引发内存泄漏。建议用try-with-resources包裹try (MockedStaticMyStaticClass mocked mockStatic(MyStaticClass.class)) { // 测试代码 }2.2 三种Mock初始化方式对比方式一手动创建适合简单测试PaymentService paymentService mock(PaymentService.class);方式二注解手动管理已过时Mock PaymentService paymentService; BeforeEach void setup() { MockitoAnnotations.openMocks(this); }方式三JUnit5扩展推荐ExtendWith(MockitoExtension.class) class PaymentTest { Mock PaymentService paymentService; Test void testPayment() { // 直接使用mock对象 } }实际项目中95%的情况推荐方式三。它自动管理mock生命周期支持构造函数注入还能通过方法参数注入Test void testWithParamInjection(Mock OrderDao orderDao) { // 每个测试方法独享mock实例 }3. Mockito核心功能实战3.1 行为模拟三板斧基础模拟when-thenReturn// 模拟查询返回特定订单 when(orderDao.findById(anyLong())) .thenReturn(Optional.of(new Order(1L, PAID))); // 模拟连续不同返回值 when(orderDao.getStatus()) .thenReturn(CREATED) .thenReturn(PAID) .thenReturn(COMPLETED);异常模拟thenThrow// 模拟支付超时 when(paymentGateway.process(any())) .thenThrow(new TimeoutException(网络超时)); // 模拟账户余额不足 when(accountService.debit(any())) .thenThrow(new BusinessException(余额不足));void方法模拟doNothing// 模拟消息发送不实际调用 doNothing().when(messageQueue).send(any());3.2 参数匹配的进阶用法精确匹配容易导致测试脆弱推荐使用参数匹配器// 任意字符串特定金额 when(paymentService.checkBalance(anyString(), eq(100.0))) .thenReturn(true); // 自定义匹配器 when(orderDao.findByCriteria(argThat(criteria - criteria.getStatus().equals(PAID)))) .thenReturn(paidOrders);注意陷阱一旦使用参数匹配器所有参数都必须用匹配器// 错误写法第二个参数没用匹配器 when(service.method(anyString(), 123)).thenReturn(...); // 正确写法 when(service.method(anyString(), eq(123))).thenReturn(...);3.3 验证调用行为验证是Mockito区别于普通桩Stub的核心能力// 基本验证 verify(orderDao).findById(1L); // 验证调用次数 verify(paymentService, times(3)).retry(any()); // 验证超时时间内完成 verify(notificationService, timeout(100)) .sendSms(any()); // 验证调用顺序 InOrder inOrder inOrder(serviceA, serviceB); inOrder.verify(serviceA).prepare(); inOrder.verify(serviceB).execute();遇到过的一个经典案例需要验证支付成功后必须且仅能调用一次记账服务verify(accountingService, times(1)) .recordPayment(eq(orderId), anyDouble()); verifyNoMoreInteractions(accountingService);4. 高级技巧Spy与依赖注入4.1 真实对象监控Spy当需要部分mock真实对象时Spy是更好的选择Spy RealService realService new RealService(); Test void testSpy() { // 真实方法会被调用 String result realService.process(input); // 可以覆盖特定方法 doReturn(mocked).when(realService).process(special); verify(realService).process(anyString()); }踩坑记录Spy对象需要初始化真实实例否则会NPE// 错误写法 Spy RealService realService; // 未初始化 // 正确写法 Spy RealService realService new RealService();4.2 依赖注入魔法InjectMocks处理复杂依赖关系的终极方案ExtendWith(MockitoExtension.class) class OrderServiceTest { Mock PaymentGateway paymentGateway; Mock InventoryService inventoryService; InjectMocks OrderService orderService; // 自动注入上述mock Test void placeOrder() { when(paymentGateway.charge(any())) .thenReturn(SUCCESS); when(inventoryService.reserve(any())) .thenReturn(true); Order order orderService.placeOrder(new Order()); assertNotNull(order.getId()); } }实际项目中的经验法则被InjectMocks标记的类会被实例化通过构造函数或setter注入Mock对象优先使用构造函数注入更明确对于复杂继承关系可能需要配合Spy使用5. 测试设计模式与最佳实践5.1 分层测试策略单元测试金字塔应用底层DAO大量使用Mockito模拟数据库驱动中间Service混合真实逻辑与mock依赖上层Controller可考虑部分集成测试WebMvcTest(OrderController.class) class OrderControllerTest { Autowired MockMvc mockMvc; MockBean OrderService orderService; Test void shouldReturnOrder() throws Exception { when(orderService.findById(anyLong())) .thenReturn(new Order(1L, PAID)); mockMvc.perform(get(/orders/1)) .andExpect(status().isOk()) .andExpect(jsonPath($.status).value(PAID)); } }5.2 可维护性技巧创建MockUtils封装常用mock逻辑class PaymentMocks { static void mockSuccess(PaymentService mock) { when(mock.process(any())) .thenReturn(new Result(true, 支付成功)); } }使用BeforeEach初始化避免重复代码BeforeEach void setup() { mockSuccess(paymentService); mockInventoryReserved(inventoryService); }自定义Answer实现复杂逻辑when(redisTemplate.opsForValue().get(anyString())) .thenAnswer(inv - { String key inv.getArgument(0); return localCache.get(key); });5.3 常见反模式过度mock把业务逻辑都mock掉失去测试意义验证过度验证每个getter/setter调用忽略线程安全在多线程测试中共享mock状态静态方法滥用导致测试相互污染记得有次排查测试偶发失败最终发现是静态mock未关闭// 错误示范忘记关闭 mockStatic(UtilityClass.class); when(UtilityClass.method()).thenReturn(...); // 正确做法 try (MockedStaticUtilityClass mocked mockStatic(UtilityClass.class)) { when(UtilityClass.method()).thenReturn(...); // 测试代码 }6. 真实项目案例订单支付流程假设我们有如下支付流程检查库存冻结库存调用支付更新订单状态扣减库存发送通知完整测试案例ExtendWith(MockitoExtension.class) class OrderPaymentTest { Mock InventoryService inventory; Mock PaymentGateway gateway; Mock NotificationService notification; InjectMocks OrderService service; Test void shouldProcessPaymentSuccessfully() { // 准备mock when(inventory.check(anyLong())).thenReturn(true); doNothing().when(inventory).freeze(anyLong()); when(gateway.charge(any())).thenReturn(SUCCESS); // 执行测试 PaymentResult result service.processPayment(1L, 100.0); // 验证行为 assertTrue(result.isSuccess()); verify(inventory).check(1L); verify(inventory).freeze(1L); verify(gateway).charge(100.0); verify(notification).sendPaymentSuccess(1L); } Test void shouldUnfreezeWhenPaymentFails() { when(inventory.check(anyLong())).thenReturn(true); when(gateway.charge(any())) .thenThrow(new PaymentException(余额不足)); assertThrows(PaymentException.class, () - service.processPayment(1L, 100.0)); verify(inventory).unfreeze(1L); } }关键验证点正向流程各环节调用顺序异常时的补偿机制如库存解冻边界条件库存不足、支付失败等并发情况下的状态一致性在电商项目中这种测试模式帮助我们发现了支付状态与库存不一致的严重bug而这一切测试都不需要启动数据库或真正的支付网关。