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> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>5.10.0</version> <scope>test</scope> </dependency> <!-- 如果你喜欢用更简洁的注解风格,可以加上这个 --> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>5.10.0</version> <scope>test</scope> </dependency>Gradle 配置:在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 是空壳,全靠你指挥(Stub);Spy 是真人,你可以部分指挥。绝大多数场景,用 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.这说明它正在启用这个功能,是正常现象。
实操心得:对于新项目,我强烈推荐
@Mock+@InjectMocks+@ExtendWith的组合拳,代码干净,管理方便。只有在你需要动态创建 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. 执行真实逻辑(仅限 Spy):thenCallRealMethod()还记得 Spy 吗?当你对 Spy 对象的某个方法打桩,但又想在某些情况下让它执行真实逻辑时使用。
List<String> realList = new ArrayList<>(); List<String> 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. 创建捕获器,指定捕获的参数类型 ArgumentCaptor<PaymentRequest> 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 (MockedStatic<StringUtils> 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 (MockedConstruction<ExternalService> mockedConstruction = mockConstruction(ExternalService.class)) { // 在这个作用域内,所有 new ExternalService(...) 的调用都会返回一个 Mock 对象 YourService service = new YourService(); service.process(); // 假设 process() 内部会 new ExternalService() // 获取到被 Mock 的实例(可能有多个) List<ExternalService> 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 对象,可以在
@BeforeAll(JUnit 5)或@BeforeClass(JUnit 4)中初始化,避免每个测试方法都重新创建。但要注意线程安全。 - 谨慎使用
@Spy和Answer:Spy和复杂的thenAnswer会带来额外的性能开销,在性能敏感的测试套件中要留意。 - 避免深度 Mock:Mockito 可以 Mock 一个接口,然后对这个 Mock 对象的方法返回另一个 Mock 对象,如此嵌套。这种“深度 Mock”会让测试逻辑难以理解,应尽量避免。如果依赖链很长,考虑是否应该重构代码,或者使用集成测试。
5. 集成测试与 Spring 生态下的 Mockito
在实际的 Spring Boot 项目中,我们通常不会直接new一个 Service 来测试,而是希望利用 Spring 的测试框架。Mockito 与 Spring Test 可以完美结合。
5.1 使用@MockBean替换 Spring 容器中的 Bean
Spring 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); // 验证支付被调用了一次,并且捕获了支付请求参数进行详细检查 ArgumentCaptor<PaymentRequest> 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。大量使用@DisplayName(JUnit 5)来让测试报告更友好。 - 不要为了 Mock 而 Mock:如果你的依赖很简单(比如一个纯计算的工具类),直接使用真实对象或许更好。Mock 的维护也是有成本的。
- 警惕测试的脆弱性:如果你的测试因为内部实现细节(比如一个无关紧要的私有方法被调用了一次还是两次)而失败,那说明你验证了不该验证的东西。测试应该关注行为(输出、状态变化、对外部的关键调用),而不是实现。
- 定期重构测试代码:测试代码也是代码,也需要保持整洁。重复的 Mock 初始化、打桩逻辑可以抽取到
@BeforeEach方法或工具类中。复杂的测试数据构建可以考虑使用Builder 模式或Object Mother模式。 - 结合覆盖率工具,但别迷信:使用 JaCoCo 等工具查看测试覆盖率,确保主要业务逻辑都被覆盖到。但 100% 的覆盖率并不代表测试是有效的,更重要的是测试用例的质量,是否覆盖了各种正常和异常的边界情况。
Mockito 是一个强大的工具,但它只是一个工具。真正的“大神”功力,体现在如何利用这个工具,编写出清晰、稳定、高效的测试,从而构建起对代码质量的坚实信心。这需要你在实践中不断思考和打磨。现在,打开你的 IDE,从为一个简单的 Service 写第一个 Mockito 测试开始吧。