
1. 项目概述为什么我们需要 Mockito如果你写过 Java 单元测试尤其是涉及数据库、网络请求或者复杂对象依赖的测试那你一定对下面这种场景不陌生你想测试一个UserService的register方法但这个方法内部调用了UserRepository去操作数据库还调用了EmailService去发送邮件。为了测试register方法本身的逻辑比如参数校验、业务规则你不得不去启动一个真实的数据库或者配置一个邮件服务器。这哪里是单元测试这简直是集成测试甚至系统测试。测试运行慢、环境依赖重、失败原因难以定位一个微小的改动可能让整个测试链崩塌。Mockito 就是为了解决这个问题而生的。它是一个流行的 Java 单元测试模拟框架核心思想就是“模拟”和“打桩”。你可以用它创建一个虚拟的、可控制的对象Mock 对象来代替那些真实、笨重、不稳定的依赖项。比如你可以创建一个UserRepository的 Mock 对象告诉它“当调用你的save方法时直接返回一个我预设好的User对象别真的去连数据库。” 这样你就能把UserService完全“隔离”出来专注测试它内部的逻辑测试速度飞快结果也稳定可靠。我见过太多团队单元测试写得痛苦不堪最后干脆不写了。引入 Mockito 这类工具是让单元测试变得可行、甚至愉快的第一步。它不仅仅是“会用几个注解”更重要的是理解“为什么要模拟”以及“如何模拟得恰到好处”。这篇指南我就从一个老码农的角度带你从零开始把 Mockito 的核心玩法、高级技巧和那些容易踩的坑一次性讲透。目标是让你看完之后不仅能写出合格的 Mock 测试更能理解背后的设计思想成为团队里那个能解决复杂测试难题的“大神”。2. 环境准备与基础概念扫盲2.1 快速搭建你的 Mockito 测试环境现在 Java 项目基本都用 Maven 或 Gradle 管理依赖添加 Mockito 非常简单。我强烈建议使用Mockito 5.x版本目前最新稳定版因为它支持了更多现代 Java 特性并且是持续维护的版本。Maven 配置在你的pom.xml文件中添加以下依赖。注意scope是test意味着它只在运行测试时生效不会打包到最终的产品中。dependency groupIdorg.mockito/groupId artifactIdmockito-core/artifactId version5.10.0/version scopetest/scope /dependency !-- 如果你喜欢用更简洁的注解风格可以加上这个 -- dependency groupIdorg.mockito/groupId artifactIdmockito-junit-jupiter/artifactId version5.10.0/version scopetest/scope /dependencyGradle 配置在build.gradle文件的dependencies块中添加testImplementation org.mockito:mockito-core:5.10.0 testImplementation org.mockito:mockito-junit-jupiter:5.10.0添加完依赖同步一下项目环境就准备好了。我建议你创建一个简单的 Java 项目跟着操作光看是记不住的。2.2 理解三个核心概念Mock, Stub, Spy在深入使用前必须厘清三个基石概念很多混淆都源于此。Mock模拟对象 这是最常用、最核心的概念。你可以把它理解为一个“傀儡”或“替身演员”。它默认什么都不会做所有方法调用返回值为 void 的方法除外都会返回“空”值如null,0,false等。你需要通过“打桩”Stubbing来告诉这个傀儡“当别人用某个参数调用你的 A 方法时你就返回 B 结果”。Mock 对象的重点在于验证交互Verification和行为控制。例如验证某个方法是否被调用、调用了几次、参数是什么。Stub打桩 这不是一个对象类型而是一个动作即“配置 Mock 对象的行为”。当你写when(mockObject.someMethod()).thenReturn(someValue)时你就是在“打桩”。你为mockObject的someMethod方法预设了行为返回某个值或抛出异常。打桩是让 Mock 对象变得有用的关键。Spy间谍对象 这是一个特殊的存在。它不是完全的“傀儡”而是一个“戴着窃听器的真实对象”。你用spy()包装一个真实存在的对象。默认情况下Spy 对象的所有方法都会委托给真实对象去执行走真实逻辑。但是你可以选择性地对它的某些方法进行“打桩”Stubbing让这些方法按你的预设行为执行而其他未被干预的方法则继续执行真实逻辑。Spy 要慎用因为它依赖于真实对象的状态如果真实对象的方法有副作用如修改数据库你用了 Spy 就可能产生意想不到的影响。通常当你只想模拟一个庞大对象的少数几个方法时才会考虑 Spy。简单总结Mock 是空壳全靠你指挥StubSpy 是真人你可以部分指挥。绝大多数场景用 Mock 就够了。3. 从零开始Mockito 核心 API 实战理论说再多不如动手。我们从一个简单的业务场景开始一个OrderService处理订单它依赖InventoryService检查库存依赖PaymentService处理支付。3.1 创建 Mock 对象的四种姿势姿势一静态方法Mockito.mock()这是最原始、最直接的方式在任何地方都能用。InventoryService inventoryServiceMock Mockito.mock(InventoryService.class); PaymentService paymentServiceMock Mockito.mock(PaymentService.class);姿势二注解Mock这是最优雅、最推荐的方式需要配合 Mockito 的初始化器使用。代码更简洁意图更清晰。import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.junit.jupiter.api.extension.ExtendWith; ExtendWith(MockitoExtension.class) // JUnit 5 的扩展 public class OrderServiceTest { Mock private InventoryService inventoryServiceMock; Mock private PaymentService paymentServiceMock; private OrderService orderService; // 待测试的真实对象 BeforeEach void setUp() { // 在测试方法执行前初始化待测对象并将 Mock 对象注入进去 orderService new OrderService(inventoryServiceMock, paymentServiceMock); } }ExtendWith(MockitoExtension.class)这个注解是魔法钥匙它会在运行测试前自动初始化所有被Mock注解标记的字段。如果你用的是 JUnit 4对应的注解是RunWith(MockitoJUnitRunner.class)。姿势三注解InjectMocks这是Mock的好搭档。它用于标记你想要测试的真实对象。Mockito 会尝试通过构造函数、setter 方法或字段反射自动将当前测试类中已有的Mock对象“注入”到这个被InjectMocks标记的对象中。ExtendWith(MockitoExtension.class) public class OrderServiceTest { Mock private InventoryService inventoryServiceMock; Mock private PaymentService paymentServiceMock; InjectMocks private OrderService orderService; // Mockito 会自动注入上面的两个 Mock // 不需要在 BeforeEach 中手动构造 orderService 了 }这大大简化了测试的搭建工作尤其是当被测试对象依赖很多时。但要注意如果存在多个匹配的依赖比如同类型有多个 Mock或者依赖注入的方式比较特殊可能需要你手动处理。姿势四内联 Mock Maker应对 Final 类和方法的挑战从 Mockito 2 开始为了提升性能和与 Java 新版本兼容默认不再能 Mockfinal类和方法、static方法以及private方法。但在某些遗留代码或第三方库中你不得不面对它们。这时就需要启用“内联 Mock Maker”。 在你的项目资源目录如src/test/resources下创建一个文件/mockito-extensions/org.mockito.plugins.MockMaker文件内容只有一行mock-maker-inline这样配置后Mockito 就能 Mock 这些“难缠”的类型了。你可能会在日志里看到一行提示Mockito is currently self-attaching to enable the inline-mock-maker.这说明它正在启用这个功能是正常现象。实操心得对于新项目我强烈推荐MockInjectMocksExtendWith的组合拳代码干净管理方便。只有在你需要动态创建 Mock或者在非测试类中使用 Mockito 时才考虑静态mock()方法。3.2 行为模拟Stubbing的十八般武艺创建了 Mock 对象接下来就要教它“做事”也就是打桩。这是 Mockito 的灵魂。1. 指定返回值thenReturn()最常用的打桩方式预设方法调用后的返回值。// 当 inventoryServiceMock.checkStock(“item123”) 被调用时返回 true when(inventoryServiceMock.checkStock(“item123”)).thenReturn(true); // 当 inventoryServiceMock.checkStock(“item456”) 被调用时返回 false when(inventoryServiceMock.checkStock(“item456”)).thenReturn(false);2. 抛出异常thenThrow()模拟依赖方法执行失败测试被测对象的异常处理逻辑。// 当 paymentServiceMock.processPayment(...) 被调用时抛出一个 RuntimeException when(paymentServiceMock.processPayment(any())).thenThrow(new RuntimeException(“Payment gateway error”));3. 执行真实逻辑仅限 SpythenCallRealMethod()还记得 Spy 吗当你对 Spy 对象的某个方法打桩但又想在某些情况下让它执行真实逻辑时使用。ListString realList new ArrayList(); ListString spiedList spy(realList); // 默认 spy 会调用真实方法 spiedList.add(“real”); // 对 size() 方法打桩让它固定返回 100 when(spiedList.size()).thenReturn(100); // 对 get(0) 方法打桩但让它调用真实逻辑需要真实列表里真的有元素 doCallRealMethod().when(spiedList).get(0);4. 连续打桩与动态返回值一个方法可以被多次打桩形成调用序列。这在模拟重试、状态变化等场景时非常有用。// 第一次调用返回 false第二次调用返回 true when(inventoryServiceMock.checkStock(“item789”)) .thenReturn(false) .thenReturn(true); // 更复杂的动态返回根据参数返回不同值甚至执行一些逻辑 when(inventoryServiceMock.getStock(anyString())).thenAnswer(invocation - { String itemId invocation.getArgument(0); if (“hot_item”.equals(itemId)) { return 100; } else { return 10; } });thenAnswer非常强大它允许你传入一个Answer接口的实现这里用了 Lambda你可以拿到调用的参数、Mock 对象本身然后执行任意逻辑来决定返回值。但能力越大责任越大别在里面写太复杂的业务逻辑否则测试本身就难以维护了。5. 为 void 方法打桩无返回值的方法不能用when(...).thenReturn(...)的语法需要用doXxx().when(...)的格式。// 模拟一个 void 方法什么都不做这是默认行为其实可以不写 doNothing().when(inventoryServiceMock).updateStock(anyString(), anyInt()); // 模拟一个 void 方法抛出异常 doThrow(new IllegalStateException(“DB down”)).when(inventoryServiceMock).updateStock(eq(“item1”), anyInt());注意事项打桩时参数匹配器的使用要小心。any(),anyString(),eq()等都是参数匹配器。一条规则如果在一个方法调用中使用了一个参数匹配器那么所有参数都必须使用匹配器。不能混用具体值和匹配器eq()除外它本身就是匹配器。// 错误示例混用了具体值 “item1” 和匹配器 anyInt() when(inventoryServiceMock.updateStock(“item1”, anyInt())).thenReturn(true); // 编译可能通过但运行行为诡异 // 正确示例全部使用匹配器 when(inventoryServiceMock.updateStock(eq(“item1”), anyInt())).thenReturn(true); // 或者全部使用具体值 when(inventoryServiceMock.updateStock(“item1”, 10)).thenReturn(true);3.3 验证交互你的代码真的按预期执行了吗Mock 对象不仅是“演员”还是“监工”。打桩定义了它的行为而验证Verification则检查它是否按照剧本被正确调用了。这是单元测试断言的重要组成部分。1. 基础验证是否被调用、调用次数// 验证 inventoryServiceMock 的 checkStock 方法被调用了一次且参数是 “item123” verify(inventoryServiceMock).checkStock(“item123”); // 验证被调用了精确的次数 verify(inventoryServiceMock, times(2)).checkStock(anyString()); // 恰好2次 verify(inventoryServiceMock, atLeastOnce()).checkStock(anyString()); // 至少1次 verify(inventoryServiceMock, atMost(5)).checkStock(anyString()); // 最多5次 verify(inventoryServiceMock, never()).deleteItem(anyString()); // 从未被调用2. 验证调用顺序有时业务逻辑要求方法必须按特定顺序执行。// 创建一个按顺序验证的验证器 InOrder inOrder inOrder(inventoryServiceMock, paymentServiceMock); // 然后严格按顺序声明验证 inOrder.verify(inventoryServiceMock).checkStock(“item123”); inOrder.verify(paymentServiceMock).processPayment(any()); // 这确保了 checkStock 必须在 processPayment 之前调用3. 验证参数捕获看看你到底传了什么有时你不仅关心方法是否被调用还关心调用时传入的参数对象内部的状态。这时就需要ArgumentCaptor。// 假设 processPayment 接受一个 PaymentRequest 对象 Mock PaymentService paymentService; Test void testOrderWithCaptor() { OrderService service new OrderService(paymentService); service.placeOrder(“user1”, “item1”, 99.9); // 1. 创建捕获器指定捕获的参数类型 ArgumentCaptorPaymentRequest captor ArgumentCaptor.forClass(PaymentRequest.class); // 2. 在 verify 时捕获参数 verify(paymentService).processPayment(captor.capture()); // 3. 获取被捕获的参数值 PaymentRequest capturedRequest captor.getValue(); // 4. 对捕获的参数进行断言 assertEquals(“user1”, capturedRequest.getUserId()); assertEquals(99.9, capturedRequest.getAmount(), 0.01); // 可以断言更复杂的对象状态 }参数捕获在验证复杂对象、或者对象在调用链中被修改后的状态时非常有用。常见问题verify太严格怎么办有时我们只关心某些关键交互不关心其他辅助性的调用。可以使用verifyNoMoreInteractions(mockObject)来检查在验证之后这个 Mock 对象是否还有任何未被验证的交互。但慎用此方法因为它会让测试变得非常脆弱——任何新增的、无关紧要的调用都会导致测试失败。通常只对在特定测试场景中需要重点关注的 Mock 对象进行精确验证即可。4. 进阶技巧与最佳实践掌握了基本操作你已经能应对 80% 的场景。但要成为“大神”还得看看下面这些进阶技巧和实践中总结出的“坑”。4.1 处理遗留代码与复杂依赖Spy 的用武之地前面说了 Spy 要慎用但有些场景它确实是救星。典型场景你有一个庞大的、历史悠久的 Service 类里面有几十个方法。你现在只想测试其中一个新方法methodA()而methodA()内部调用了同一个类里的另一个方法helperMethod()。helperMethod本身很复杂或者有外部依赖比如调用了数据库你不想为了测试methodA而把整个helperMethod的逻辑都走一遍。这时你可以对这个庞大的 Service 对象创建一个Spy然后只对helperMethod()进行打桩让methodA()的其他逻辑正常执行。public class LegacyBigService { public String methodA(String input) { // ... 一些逻辑 ... String intermediate helperMethod(input); // 这个方法我们想模拟 // ... 更多逻辑基于 intermediate 处理 ... return result; } public String helperMethod(String input) { // 非常复杂的逻辑或者有网络、数据库调用 return “complex result from DB”; } } Test void testMethodAWithSpy() { // 创建真实对象 LegacyBigService realService new LegacyBigService(); // 用 spy 包装它 LegacyBigService spiedService spy(realService); // 只对 helperMethod 打桩让它返回一个预设值 doReturn(“mocked helper result”).when(spiedService).helperMethod(anyString()); // 调用 methodA它会使用真实的 methodA 逻辑但其中调用的 helperMethod 已被我们“劫持” String finalResult spiedService.methodA(“test input”); // 断言 finalResult 是否符合预期基于我们模拟的 helperMethod 返回值 assertEquals(“expected result based on mocked helper”, finalResult); }重要警告使用doReturn(...).when(...)而不是when(...).thenReturn(...)来给 Spy 对象的方法打桩。因为后者when(spiedService.helperMethod(...))会先真实地调用一次helperMethod这很可能不是你想要的比如会触发数据库连接。doReturn模式则避免了这次真实调用。4.2 静态方法、Final 方法与构造函数的 Mock对于现代、良好的代码设计静态方法、final 方法的使用是克制的构造函数更不应该在业务逻辑中直接new对象依赖注入是更好的选择。但现实是骨感的你总会遇到需要 Mock 它们的时候。1. 静态方法使用 Mockito 的mockStatic需要mockito-inline假设你有一个讨厌的静态工具类StringUtils。try (MockedStaticStringUtils mockedStatic mockStatic(StringUtils.class)) { // 在 try-with-resources 块内可以对这个类的静态方法打桩 mockedStatic.when(() - StringUtils.isEmpty(anyString())).thenReturn(false); // 现在任何调用 StringUtils.isEmpty 的地方都会返回 false YourService service new YourService(); service.doSomething(“test”); // 内部调用了 StringUtils.isEmpty // 也可以验证静态方法的调用 mockedStatic.verify(() - StringUtils.isEmpty(eq(“test”))); } // 一旦离开这个 try 块静态方法的 Mock 就会自动关闭恢复原样关键点MockedStatic对象必须用try-with-resources或手动close()来管理作用域确保 Mock 效果不会泄露到其他测试中。2. Final 方法和类只要按照 3.1 节启用了内联 Mock Maker (mock-maker-inline)创建它们的 Mock 对象就和普通类一样直接用Mock或mock()即可无需特殊语法。3. 构造函数使用Mockito.mockConstruction需要mockito-inline这是更高级的场景用于模拟在待测代码内部new出来的对象。try (MockedConstructionExternalService mockedConstruction mockConstruction(ExternalService.class)) { // 在这个作用域内所有 new ExternalService(...) 的调用都会返回一个 Mock 对象 YourService service new YourService(); service.process(); // 假设 process() 内部会 new ExternalService() // 获取到被 Mock 的实例可能有多个 ListExternalService constructedMocks mockedConstruction.constructed(); // 对第一个被创建的 Mock 实例进行验证或打桩 verify(constructedMocks.get(0)).someMethod(); }这个功能非常强大但也要慎用。它通常意味着你的代码设计有改进空间比如应该通过依赖注入传入ExternalService的实例。4.3 测试驱动开发TDD中的 Mockito 心法Mockito 是实践 TDD 的利器。在 TDD 的“红-绿-重构”循环中红写失败测试你首先定义被测对象的行为方法签名、返回值并为其依赖定义接口。此时你可以直接用 Mockito 创建这些依赖的 Mock并编写测试用例。测试自然是失败的因为你还没实现功能。绿实现功能以实现最简单代码让测试通过为目标。在这个过程中你之前用 Mockito 定义的交互契约比如A方法应该调用B服务的C方法一次就是你的实现指南。重构在测试的保护下你可以放心地改进代码结构。Mockito 的验证机制能确保你的重构没有破坏已有的交互逻辑。心法要点Mock 你无法控制的东西而不是你不想实现的东西。比如数据库、第三方 API、文件系统、当前时间 (new Date()) 等这些是“无法控制”的依赖应该 Mock。而你的业务逻辑类、工具类这些是“可以控制”的应该用真实实现或 Spy并进行单元测试。过度 Mock 会导致测试与实现耦合过紧失去了测试的意义。4.4 常见陷阱与性能优化陷阱一错误的打桩顺序Mockito 的打桩是“后定义覆盖前定义”。但更隐蔽的问题是如果你先调用真实方法再打桩那打桩是无效的。Mockito 的工作机制是在方法被调用时查找匹配的打桩定义。所以打桩一定要在调用被测方法之前完成。陷阱二过度验证不要验证每一个 Mock 对象的每一次调用。只验证那些对当前测试用例的业务逻辑至关重要的交互。过度验证会让测试变得脆弱难以重构。记住单元测试是验证行为而不是验证实现细节。陷阱三Mock 真实对象永远不要 Mock 你要测试的那个类即InjectMocks标记的类。你也应该尽量避免 Mock 值对象如String,Integer, 你自己的DTO。Mock 是用来模拟有行为的对象服务、仓库、组件而不是模拟数据载体。性能优化复用 Mock 对象对于不会改变状态的 Mock 对象可以在BeforeAllJUnit 5或BeforeClassJUnit 4中初始化避免每个测试方法都重新创建。但要注意线程安全。谨慎使用Spy和AnswerSpy和复杂的thenAnswer会带来额外的性能开销在性能敏感的测试套件中要留意。避免深度 MockMockito 可以 Mock 一个接口然后对这个 Mock 对象的方法返回另一个 Mock 对象如此嵌套。这种“深度 Mock”会让测试逻辑难以理解应尽量避免。如果依赖链很长考虑是否应该重构代码或者使用集成测试。5. 集成测试与 Spring 生态下的 Mockito在实际的 Spring Boot 项目中我们通常不会直接new一个 Service 来测试而是希望利用 Spring 的测试框架。Mockito 与 Spring Test 可以完美结合。5.1 使用MockBean替换 Spring 容器中的 BeanSpring Boot Test 提供了MockBean注解。它会将指定的 Bean 替换为一个 Mockito Mock 对象并注册到 Spring 的测试应用上下文中。这是集成测试中部分模拟的利器。SpringBootTest // 启动一个轻量级的 Spring 容器 class OrderServiceIntegrationTest { Autowired private OrderService orderService; // 注入真实的待测服务 MockBean // 这个注解会将容器中的 InventoryService Bean 替换为一个 Mock private InventoryService inventoryServiceMock; MockBean private PaymentService paymentServiceMock; Test void testPlaceOrderWithMockedDependencies() { // 为 Mock Bean 打桩 when(inventoryServiceMock.checkStock(“item1”)).thenReturn(true); when(paymentServiceMock.processPayment(any())).thenReturn(“SUCCESS_123”); // 调用真实的 orderService它会自动注入上面两个 Mock Bean OrderResult result orderService.placeOrder(new OrderRequest(“item1”, 1)); // 断言业务结果 assertTrue(result.isSuccess()); // 验证交互 verify(inventoryServiceMock).checkStock(“item1”); verify(paymentServiceMock).processPayment(any()); } }MockBean非常方便但它会真正启动 Spring 上下文速度比纯单元测试慢。它适用于切片测试如只测试 Web 层WebMvcTest或只测试数据层DataJpaTest或需要部分真实集成如测试 Controller 时Service 层用 Mock的场景。5.2 纯单元测试 vs 集成测试中的 Mock要明确区分两种测试策略纯单元测试Pure Unit Test使用ExtendWith(MockitoExtension.class)不启动 Spring。速度极快只测试单个类及其直接依赖的 Mock。这是你应该写得最多的测试。集成测试Integration Test with Mock使用SpringBootTest和MockBean。速度较慢但能测试 Spring 的依赖注入、事务管理、AOP 切面等容器特性。用于测试模块间的集成或者当你需要 Mock 掉某个外部系统如第三方 API 客户端时。最佳实践构建一个测试金字塔。底层是大量快速的纯单元测试使用 Mockito中间是较少的中等速度的集成测试使用MockBean顶层是极少数的慢速端到端测试。Mockito 主要活跃在金字塔的底层和中下层。6. 复杂场景示例一个完整的订单流程测试让我们把所有知识串联起来写一个相对完整的测试案例。假设我们有如下类Order订单实体。InventoryService库存服务接口有checkStock和reduceStock方法。PaymentService支付服务接口有processPayment方法。NotificationService通知服务接口有sendOrderConfirmed方法。OrderService我们的被测服务包含placeOrder核心逻辑。OrderService.placeOrder逻辑检查库存。如果库存不足抛出异常。如果库存充足则处理支付。支付成功则减少库存发送确认通知返回成功订单。支付失败抛出异常库存不减。ExtendWith(MockitoExtension.class) class OrderServiceCompleteTest { Mock private InventoryService inventoryService; Mock private PaymentService paymentService; Mock private NotificationService notificationService; InjectMocks private OrderService orderService; Test void placeOrder_Success() { // 1. 准备测试数据 String itemId “Laptop-001”; int quantity 1; BigDecimal price new BigDecimal(“9999.99”); Order order new Order(itemId, quantity, price); String paymentTxId “TX-2024-001”; // 2. 为依赖的 Mock 对象打桩 when(inventoryService.checkStock(itemId, quantity)).thenReturn(true); when(paymentService.processPayment(any(PaymentRequest.class))).thenReturn(paymentTxId); // void 方法默认就是 doNothing可以不写。这里为了清晰还是写上。 doNothing().when(inventoryService).reduceStock(itemId, quantity); doNothing().when(notificationService).sendOrderConfirmed(any(Order.class)); // 3. 执行被测方法 Order result orderService.placeOrder(order); // 4. 断言结果 assertNotNull(result); assertEquals(OrderStatus.CONFIRMED, result.getStatus()); assertEquals(paymentTxId, result.getPaymentTransactionId()); assertNotNull(result.getConfirmedAt()); // 5. 验证交互行为非常重要 // 验证库存检查被调用了一次 verify(inventoryService, times(1)).checkStock(itemId, quantity); // 验证支付被调用了一次并且捕获了支付请求参数进行详细检查 ArgumentCaptorPaymentRequest paymentCaptor ArgumentCaptor.forClass(PaymentRequest.class); verify(paymentService, times(1)).processPayment(paymentCaptor.capture()); PaymentRequest actualRequest paymentCaptor.getValue(); assertEquals(order.getTotalAmount(), actualRequest.getAmount()); // 验证减库存被调用了一次 verify(inventoryService, times(1)).reduceStock(itemId, quantity); // 验证发送通知被调用了一次 verify(notificationService, times(1)).sendOrderConfirmed(result); // 验证没有其他不必要的交互 verifyNoMoreInteractions(inventoryService, paymentService, notificationService); } Test void placeOrder_OutOfStock() { Order order new Order(“SoldOutItem”, 10, BigDecimal.ONE); when(inventoryService.checkStock(“SoldOutItem”, 10)).thenReturn(false); // 断言会抛出特定的业务异常 assertThrows(InsufficientStockException.class, () - orderService.placeOrder(order)); // 验证只检查了库存没有进行支付、减库存等操作 verify(inventoryService).checkStock(“SoldOutItem”, 10); verify(paymentService, never()).processPayment(any()); verify(inventoryService, never()).reduceStock(anyString(), anyInt()); verify(notificationService, never()).sendOrderConfirmed(any()); } Test void placeOrder_PaymentFailed() { Order order new Order(“Item1”, 1, BigDecimal.TEN); when(inventoryService.checkStock(“Item1”, 1)).thenReturn(true); when(paymentService.processPayment(any())).thenThrow(new PaymentFailedException(“Card declined”)); assertThrows(PaymentFailedException.class, () - orderService.placeOrder(order)); // 关键验证支付失败后库存不应该被减少 verify(inventoryService).checkStock(“Item1”, 1); verify(paymentService).processPayment(any()); verify(inventoryService, never()).reduceStock(anyString(), anyInt()); // 这行很重要 verify(notificationService, never()).sendOrderConfirmed(any()); } }这个测试案例展示了清晰的 Given-When-Then 结构准备数据Given、执行操作When、验证结果和交互Then。全面的验证不仅验证返回值还通过verify验证了关键的交互逻辑特别是支付失败后不减库存这个业务规则。异常流的测试专门测试了库存不足和支付失败的场景。参数捕获的使用在成功流程中捕获了PaymentRequest对象进行更细致的断言。7. 总结与最后的建议走到这里你已经从“知道 Mockito 是什么”升级到了“能在实际项目中合理运用 Mockito 设计测试”。最后我再分享几条从无数坑里爬出来的经验保持测试的单一性和可读性一个测试方法只测一个场景或一个分支。测试方法名应该清晰地表达其意图比如placeOrder_Success,placeOrder_OutOfStock。大量使用DisplayNameJUnit 5来让测试报告更友好。不要为了 Mock 而 Mock如果你的依赖很简单比如一个纯计算的工具类直接使用真实对象或许更好。Mock 的维护也是有成本的。警惕测试的脆弱性如果你的测试因为内部实现细节比如一个无关紧要的私有方法被调用了一次还是两次而失败那说明你验证了不该验证的东西。测试应该关注行为输出、状态变化、对外部的关键调用而不是实现。定期重构测试代码测试代码也是代码也需要保持整洁。重复的 Mock 初始化、打桩逻辑可以抽取到BeforeEach方法或工具类中。复杂的测试数据构建可以考虑使用Builder 模式或Object Mother模式。结合覆盖率工具但别迷信使用 JaCoCo 等工具查看测试覆盖率确保主要业务逻辑都被覆盖到。但 100% 的覆盖率并不代表测试是有效的更重要的是测试用例的质量是否覆盖了各种正常和异常的边界情况。Mockito 是一个强大的工具但它只是一个工具。真正的“大神”功力体现在如何利用这个工具编写出清晰、稳定、高效的测试从而构建起对代码质量的坚实信心。这需要你在实践中不断思考和打磨。现在打开你的 IDE从为一个简单的 Service 写第一个 Mockito 测试开始吧。