从Mockito的`when().thenReturn()`到`verify()`:一份写给SpringBoot开发者的单元测试行为验证手册
从Mockito的when().thenReturn()到verify()一份写给SpringBoot开发者的单元测试行为验证手册在SpringBoot项目的日常开发中单元测试往往被简化为对方法返回值的简单断言。但真正的测试艺术在于验证代码的行为契约——这个方法是否按预期被调用调用次数是否符合要求参数传递是否正确Mockito框架提供的when().thenReturn()和verify()组合正是实现这种深度验证的瑞士军刀。本文将带你超越基础的返回值断言通过六个渐进式案例掌握如何用Mockito构建具有行为验证能力的专业级单元测试。无论你是需要模拟数据库异常、验证异步回调还是检查方法调用顺序这些技巧都能让你的测试代码更具表达力和可靠性。1. 环境准备与基础配置1.1 依赖配置SpringBoot项目只需引入spring-boot-starter-test它已包含Mockito核心库dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency对于非SpringBoot项目可单独引入Mockitodependency groupIdorg.mockito/groupId artifactIdmockito-core/artifactId version4.4.0/version scopetest/scope /dependency1.2 测试类基础结构现代SpringBoot测试类推荐使用JUnit JupiterJUnit 5配合Mockito扩展ExtendWith(MockitoExtension.class) class OrderServiceTest { Mock private PaymentGateway paymentGateway; InjectMocks private OrderService orderService; // 测试用例将在这里编写 }关键注解说明注解作用生命周期Mock创建模拟对象每个测试方法前重新初始化InjectMocks创建被测对象并注入mock依赖与方法级BeforeEach效果相同Spy部分模拟真实对象可混合真实方法与模拟方法2. 行为模拟的三重境界2.1 基础返回值模拟最常见的when().thenReturn()模式适用于有返回值的方法Test void shouldReturnDiscountPrice() { // 模拟商品服务返回指定价格 when(productService.getPrice(VIP001)).thenReturn(299.00); double finalPrice orderService.calculateFinalPrice(VIP001); assertThat(finalPrice).isEqualTo(269.10); // 假设打9折 }参数匹配器的妙用// 任意字符串参数都返回相同结果 when(userRepository.findByUsername(anyString())).thenReturn(Optional.of(defaultUser)); // 特定参数值触发不同响应 when(apiClient.post(eq(/vip), any(VipRequest.class))) .thenReturn(successResponse);2.2 异常场景模拟使用thenThrow()模拟方法抛出异常Test void shouldThrowWhenInventoryInsufficient() { when(inventoryService.checkStock(anyString(), gt(10))) .thenThrow(new InventoryException(Not enough stock)); assertThatThrownBy(() - orderService.placeOrder(bulkOrder)) .isInstanceOf(OrderException.class) .hasMessageContaining(库存不足); }对于void方法需使用doThrow()模式Test void shouldLogErrorWhenNotificationFails() { doThrow(new NotificationException(Service unavailable)) .when(notificationService).sendSms(any()); orderService.confirmOrder(ORD123); verify(errorLogger).log(contains(短信发送失败)); }2.3 动态响应与连续调用通过thenAnswer()实现动态响应Test void shouldGenerateSequentialOrderNumbers() { when(idGenerator.nextId()) .thenAnswer(inv - ORD- System.currentTimeMillis()); String id1 orderService.createOrder(); String id2 orderService.createOrder(); assertThat(id1).isNotEqualTo(id2); }模拟多次调用的不同返回Test void shouldRetryOnFirstFailure() { when(thirdPartyApi.getStatus(any())) .thenThrow(new TimeoutException()) .thenReturn(PROCESSING) .thenReturn(COMPLETED); String result orderService.syncOrderStatus(ORD123); assertThat(result).isEqualTo(COMPLETED); }3. 行为验证的精妙控制3.1 基础验证方法verify()的基本用法验证方法是否被调用Test void shouldCallPaymentGateway() { orderService.processPayment(ORD123, 100.00); verify(paymentGateway).charge(anyString(), eq(100.00)); }验证调用次数Test void shouldRetryThreeTimesOnFailure() { when(apiClient.get(any())).thenThrow(new RuntimeException()); try { dataSyncService.sync(); } catch (Exception ignored) {} verify(apiClient, times(3)).get(any()); }常用验证模式方法等效写法说明times(1)默认行为恰好调用1次atLeastOnce()atLeast(1)至少1次atMost(3)-最多3次never()times(0)从未调用3.2 参数捕获与深度断言使用ArgumentCaptor捕获方法参数进行详细断言Test void shouldPassCorrectAuditInfo() { Order order testOrderBuilder.build(); orderService.approveOrder(order.getId()); ArgumentCaptorAuditLog captor ArgumentCaptor.forClass(AuditLog.class); verify(auditService).log(captor.capture()); AuditLog log captor.getValue(); assertThat(log.getAction()).isEqualTo(APPROVE); assertThat(log.getOperator()).isEqualTo(SecurityContext.getCurrentUser()); }3.3 调用顺序验证通过InOrder验证调用顺序Test void shouldFollowCorrectWorkflow() { Order order testOrderBuilder.build(); orderService.process(order); InOrder inOrder inOrder( inventoryService, paymentService, shippingService ); inOrder.verify(inventoryService).lockStock(order); inOrder.verify(paymentService).charge(order); inOrder.verify(shippingService).scheduleDelivery(order); }4. 复杂场景实战技巧4.1 void方法的回调验证验证void方法的回调行为Test void shouldHandleAsyncCallback() { doAnswer(inv - { Runnable callback inv.getArgument(1); callback.run(); return null; }).when(asyncService).execute(any(), any(Runnable.class)); orderService.asyncProcess(); verify(orderStatusUpdater).markAsCompleted(); }4.2 超时验证验证异步操作是否在指定时间内完成Test void shouldCompleteWithinTimeout() { orderService.startBackgroundJob(); verify(backgroundJobMonitor, timeout(1000)) .notifyCompletion(); }4.3 静态方法模拟需Mockito-inline对于静态工具类方法的模拟Test void shouldMockStaticMethod() { try (MockedStaticIdGenerator mocked mockStatic(IdGenerator.class)) { mocked.when(IdGenerator::generate).thenReturn(FIXED-ID); String id orderService.createOrder(); assertThat(id).isEqualTo(ORDER-FIXED-ID); } }5. SpringBoot集成特别注意事项5.1 上下文感知测试当需要部分真实Spring上下文时SpringBootTest(classes {OrderService.class, TestConfig.class}) AutoConfigureMockMvc class OrderServiceIntegrationTest { MockBean // Spring特有的mock注解 private PaymentRepository paymentRepository; Autowired private OrderService orderService; }5.2 事务处理策略测试类默认会回滚事务可通过以下配置调整Transactional Commit // 或 Rollback(false) Test void shouldPersistAuditLog() { orderService.approveOrder(ORD123); assertThat(auditLogRepository.count()).isEqualTo(1); }6. 测试代码质量提升指南6.1 构建可读性高的验证避免过度验证导致测试脆弱// 不推荐 - 过度指定参数细节 verify(emailService).send( eq(orderexample.com), eq(Order Confirmation), contains(ORD123), any(LocalDateTime.class) ); // 推荐 - 关注核心契约 verify(emailService).send( anyString(), startsWith(Order), contains(order.getId()) );6.2 自定义参数匹配器创建领域特定的匹配器Test void shouldApplyVIPDiscount() { orderService.processVIPOrder(VIP_ORDER); verify(pricingService).applyDiscount(argThat(order - order.getItems().size() 2 order.getUser().isVIP() )); }6.3 验证模式最佳实践推荐组合对被测对象的输出结果进行断言对协作对象的关键行为进行验证避免验证实现细节Test void shouldCompleteOrderHappyPath() { // 准备 when(inventoryService.reserve(any())).thenReturn(true); when(paymentService.charge(any())).thenReturn(PAID); // 执行 OrderResult result orderService.completeOrder(TEST_ORDER); // 验证 assertThat(result.getStatus()).isEqualTo(COMPLETED); verify(shippingService).schedule(any()); verifyNoMoreInteractions(notificationService); // 不应调用 }