别再乱用Mockito了盘点单元测试中Mock、Spy、Stub的5个典型误用场景与正确写法单元测试是保障代码质量的重要防线而Mockito作为Java生态中最流行的测试框架之一其灵活性和强大功能让开发者爱不释手。但就像一把双刃剑如果使用不当Mockito反而会成为测试代码的毒药。本文将深入剖析五个最常见的误用场景帮助开发者避开这些陷阱写出更健壮、更有价值的测试代码。1. 过度Mock当测试失去验证意义过度Mock是单元测试中最常见的反模式之一。很多开发者习惯性地将所有依赖都Mock掉导致测试变成了皇帝的新装——看似在测试实则什么都没验证。典型错误示例Test public void testOrderProcessing() { // 过度Mock将所有依赖都替换为Mock对象 PaymentService paymentService Mockito.mock(PaymentService.class); InventoryService inventoryService Mockito.mock(InventoryService.class); ShippingService shippingService Mockito.mock(ShippingService.class); OrderService orderService new OrderService(paymentService, inventoryService, shippingService); // 设置所有Mock行为 when(paymentService.process(any())).thenReturn(true); when(inventoryService.reserve(any())).thenReturn(true); when(shippingService.schedule(any())).thenReturn(TRACK123); Order order new Order(user123, List.of(item1, item2)); OrderResult result orderService.process(order); assertTrue(result.isSuccess()); }这段测试的问题在于它实际上只验证了OrderService能否调用这些Mock对象而没有验证任何业务逻辑。如果OrderService的实现完全错误比如根本不检查库存测试仍然会通过。正确实践原则只Mock真正的外部依赖如数据库、第三方API保留领域对象和业务逻辑的真实实现。改进后的测试Test public void testOrderProcessingWithRealLogic() { // 只Mock真正的外部服务 PaymentGateway paymentGateway Mockito.mock(PaymentGateway.class); ShippingProvider shippingProvider Mockito.mock(ShippingProvider.class); // 使用真实的领域服务 InventoryManager inventoryManager new RealInventoryManager(); OrderValidator validator new OrderValidator(); OrderService orderService new OrderService( paymentGateway, inventoryManager, shippingProvider, validator ); // 准备测试数据 when(paymentGateway.charge(any())).thenReturn(new PaymentResult(true)); when(shippingProvider.createShipment(any())).thenReturn(TRACK123); Order order createTestOrder(); OrderResult result orderService.process(order); // 验证业务逻辑而不仅仅是方法调用 assertTrue(result.isSuccess()); assertEquals(2, inventoryManager.getStock(item1)); // 验证库存真实变化 }关键区别保留了核心业务逻辑的真实实现只Mock了真正的外部依赖支付网关、物流服务验证了业务状态的实际变化库存数量2. Mock与Spy的混淆何时该用哪个Mock和Spy是Mockito提供的两种不同测试替身但很多开发者经常混淆它们的使用场景导致测试要么过于宽松要么过于严格。典型误用场景Test public void testDataProcessor() { // 错误使用Mock替代真实对象 DataTransformer transformer Mockito.mock(DataTransformer.class); when(transformer.transform(any())).thenReturn(transformed); DataProcessor processor new DataProcessor(transformer); String result processor.process(input); assertEquals(TRANSFORMED, result); // 假设processor会转为大写 }这里的问题在于如果DataProcessor的真实逻辑是对DataTransformer的结果进行大写转换那么用Mock完全替代DataTransformer会导致我们无法测试这部分逻辑。正确选择Mock vs Spy特性MockSpy默认行为所有方法返回null或默认值调用真实方法适用场景完全替代依赖部分修改真实对象行为性能更轻量需要实例化真实对象典型用例外部服务、接口需要测试部分真实逻辑的复杂对象改进后的测试应该使用SpyTest public void testDataProcessorWithSpy() { // 使用Spy包装真实对象 DataTransformer realTransformer new DataTransformer(); DataTransformer transformer Mockito.spy(realTransformer); // 只Stub需要修改的方法 doReturn(transformed).when(transformer).transform(any()); DataProcessor processor new DataProcessor(transformer); String result processor.process(input); // 验证processor的真实逻辑大写转换 assertEquals(TRANSFORMED, result); // 可以验证交互 verify(transformer).transform(input); }经验法则当需要完全控制依赖行为时 → 用Mock当需要测试对象的部分真实行为时 → 用Spy当依赖是简单值对象时 → 不要用任何替身直接用真实对象3. Stubbing陷阱脆弱的测试契约过度指定或过于详细的Stubbing是另一个常见问题这会导致测试变得脆弱——实现细节的微小变化就会导致测试失败即使整体行为是正确的。脆弱测试示例Test public void testUserNotification() { NotificationService notificationService Mockito.mock(NotificationService.class); UserService userService new UserService(notificationService); // 过于详细的Stubbing when(notificationService.sendEmail( eq(userexample.com), eq(Welcome), contains(Dear User)) ).thenReturn(true); userService.register(new User(userexample.com, User)); verify(notificationService).sendEmail(any(), any(), any()); }这段测试的问题在于对邮件内容和主题的验证过于严格如果欢迎邮件的文案稍有变化测试就会失败实际上我们只关心是否发送了通知而不是具体内容健壮的Stubbing策略改进后的版本Test public void testUserNotificationWithFlexibleStubbing() { NotificationService notificationService Mockito.mock(NotificationService.class); UserService userService new UserService(notificationService); // 更灵活的Stubbing when(notificationService.sendEmail( anyString(), startsWith(Welcome), anyString()) ).thenReturn(true); User user new User(userexample.com, User); userService.register(user); // 验证核心契约而非实现细节 verify(notificationService).sendEmail( user.getEmail(), anyString(), anyString() ); }Stubbing最佳实践宽松参数匹配优先使用any(),anyString()等宽松匹配器验证核心契约只验证关键参数如用户邮箱避免过度指定不要Stub不需要的方法使用自定义匹配器对于复杂验证可以创建自定义ArgumentMatcher// 自定义匹配器示例 verify(notificationService).sendEmail( argThat(email - email.endsWith(example.com)), argThat(subject - subject.startsWith(Welcome)), anyString() );4. Verify滥用测试变得脆弱Verify是Mockito的强大功能但过度使用会导致测试变得极其脆弱——任何内部实现的变化都会导致测试失败即使外部行为没有改变。过度验证的典型例子Test public void testReportGeneration() { DataFetcher fetcher Mockito.mock(DataFetcher.class); when(fetcher.fetchData()).thenReturn(testData()); ReportGenerator generator new ReportGenerator(fetcher); Report report generator.generateMonthlyReport(); // 过度验证实现细节 verify(fetcher).fetchData(); verify(fetcher, times(1)).connect(); verify(fetcher, times(1)).disconnect(); verifyNoMoreInteractions(fetcher); assertNotNull(report); }这段测试的问题在于它过度关注ReportGenerator的内部实现方式而不是它生成报告的能力。如果重构ReportGenerator使其使用不同的数据获取策略即使生成的报告完全正确测试也会失败。更健壮的验证策略改进后的版本Test public void testReportGenerationFocusOnOutput() { DataFetcher fetcher Mockito.mock(DataFetcher.class); when(fetcher.fetchData()).thenReturn(testData()); ReportGenerator generator new ReportGenerator(fetcher); Report report generator.generateMonthlyReport(); // 只验证必要的交互 verify(fetcher, atLeastOnce()).fetchData(); // 主要验证输出结果 assertNotNull(report); assertEquals(5, report.getSectionCount()); assertTrue(report.getMetrics().containsKey(revenue)); }Verify使用原则验证关键契约只验证必须发生的交互避免精确计数慎用times(N)除非调用次数确实是业务需求关注输出而非实现优先验证方法的结果而非内部调用宽松验证使用atLeastOnce()、atMostOnce()等代替固定次数提示如果一个测试中verify调用超过3次很可能已经过度验证实现细节了。5. 静态方法与并发陷阱Mockito在处理静态方法和并发场景时有一些限制和陷阱很多开发者在使用时容易踩坑。静态方法的问题示例public class OrderUtils { public static boolean validateOrder(Order order) { // 复杂的验证逻辑 } } public class OrderService { public OrderResult process(Order order) { if (!OrderUtils.validateOrder(order)) { return OrderResult.error(Invalid order); } // 处理逻辑 } }测试时很多开发者会尝试Mock静态方法Test public void testOrderProcessingWithStaticMock() { try (MockedStaticOrderUtils mocked Mockito.mockStatic(OrderUtils.class)) { mocked.when(() - OrderUtils.validateOrder(any())).thenReturn(true); OrderService service new OrderService(); OrderResult result service.process(testOrder()); assertTrue(result.isSuccess()); } }虽然Mockito 3.4支持静态方法Mock但这通常是一个设计异味——过度使用静态方法会导致代码难以测试和维护。更好的解决方案依赖注入替代静态方法public class OrderService { private final OrderValidator validator; public OrderService(OrderValidator validator) { this.validator validator; } public OrderResult process(Order order) { if (!validator.validate(order)) { return OrderResult.error(Invalid order); } // 处理逻辑 } }并发测试注意事项Mock对象默认不是线程安全的。如果在多线程测试中使用Mock对象可能会遇到意外行为。Test public void testConcurrentAccess() { CounterService counter Mockito.mock(CounterService.class); when(counter.increment()).thenCallRealMethod(); // 危险 // 多线程访问mock对象可能导致不确定行为 ExecutorService executor Executors.newFixedThreadPool(10); for (int i 0; i 10; i) { executor.submit(() - counter.increment()); } // 断言可能不可靠 verify(counter, times(10)).increment(); }并发测试最佳实践避免在多线程测试中共享Mock对象使用真实的线程安全对象进行并发测试如果必须Mock确保Stubbing在测试开始前完成考虑使用ConcurrentHashMap等线程安全集合作为测试替身提升测试质量的实用技巧除了避免上述误用场景外以下技巧可以帮助你编写更高质量的测试代码命名约定测试方法名应该明确表达测试的意图如shouldReturnErrorWhenInventoryIsEmpty单一断言原则每个测试应该只验证一个行为多个断言应该验证同一逻辑单元的多个方面测试数据构建器使用Builder模式创建测试数据提高可读性和可维护性User testUser new UserBuilder() .withEmail(testexample.com) .withName(Test User) .withRole(Role.ADMIN) .build();行为驱动开发(BDD)风格使用given-when-then结构使测试更易读Test public void shouldApplyDiscountForPremiumUsers() { // Given User premiumUser createPremiumUser(); Order order createOrderWithItems(premiumUser); // When OrderResult result orderService.process(order); // Then assertThat(result.getTotal()).isLessThan(order.getSubtotal()); }自定义Mockito插件对于复杂场景可以创建自定义Answer实现when(repository.find(any())).thenAnswer(invocation - { String id invocation.getArgument(0); return createTestEntity(id); });