Mockito 深度实战——Mock/Spy/Captor 核心机制与常见误区
Mockito 深度实战——Mock/Spy/Captor 核心机制与常见误区
适读人群:用 Mockito 写过测试但总是遇到奇怪问题、不确定什么时候用 Mock 什么时候用 Spy 的 Java 工程师 | 阅读时长:约14分钟 | 核心价值:彻底搞清楚 Mock、Spy、Captor 的本质,不再靠猜
那次被 Mock 坑惨的经历
2020年我刚开始在团队里推广单元测试,一个同事来找我,说他的测试有问题。
他的被测代码大概是这样:
public class NotificationService {
private final EmailSender emailSender;
private final SmsSender smsSender;
public void notifyUser(User user, String message) {
if (user.getEmail() != null) {
emailSender.send(user.getEmail(), message);
}
if (user.getPhone() != null) {
smsSender.send(user.getPhone(), message);
}
}
}他的测试:
@Test
void testNotifyUser() {
EmailSender emailSender = mock(EmailSender.class);
SmsSender smsSender = mock(SmsSender.class);
NotificationService service = new NotificationService(emailSender, smsSender);
User user = new User("张三", "zhangsan@test.com", null); // 只有邮件,没有手机号
service.notifyUser(user, "你好");
verify(emailSender).send("zhangsan@test.com", "你好");
verify(smsSender).send(any(), any()); // 这里会失败!
}他不明白为什么最后一个 verify 失败了——"我以为 mock 之后,所有方法调用都会被记录啊?"
我说:是的,会记录,但你 verify(smsSender).send(any(), any()) 意思是验证 smsSender.send 被调用了一次,而实际上 user 没有手机号,smsSender.send 根本没被调用。
他恍然大悟。但这个问题引出了一个更根本的问题:Mockito 里的 Mock、Spy 到底是什么,它们的行为边界在哪里?
Mock 的本质:替换掉真实实现
mock(SomeClass.class) 创建的对象,是 SomeClass 的一个代理对象,它:
- 不执行任何真实方法——所有方法默认返回空值(null、0、false、空集合)
- 记录所有方法调用——调用过什么、传了什么参数,都被记录下来
- 可以设定返回值——用
when(...).thenReturn(...)指定特定调用的返回值
@Test
void testMockBehavior() {
// 创建 mock 对象
UserRepository mockRepo = mock(UserRepository.class);
// 未设定 stub 时,返回默认值
User result = mockRepo.findById(1L); // 返回 null
List<User> list = mockRepo.findAll(); // 返回空 List(不是 null!)
int count = mockRepo.count(); // 返回 0
boolean exists = mockRepo.existsById(1L); // 返回 false
// 设定 stub:指定特定调用的返回值
User mockUser = new User(1L, "张三");
when(mockRepo.findById(1L)).thenReturn(Optional.of(mockUser));
// 现在调用返回我们设定的值
Optional<User> found = mockRepo.findById(1L);
assertTrue(found.isPresent());
assertEquals("张三", found.get().getName());
// 未 stub 的调用仍然返回默认值
Optional<User> notFound = mockRepo.findById(999L);
assertFalse(notFound.isPresent());
}Stub 的高级用法
// 根据参数动态返回值
when(mockRepo.findById(anyLong())).thenAnswer(invocation -> {
Long id = invocation.getArgument(0);
if (id > 0) return Optional.of(new User(id, "用户" + id));
return Optional.empty();
});
// 连续调用返回不同值
when(mockRepo.findAll())
.thenReturn(List.of(user1)) // 第1次调用
.thenReturn(List.of(user1, user2)) // 第2次调用
.thenReturn(Collections.emptyList()); // 第3次及以后
// 模拟抛出异常
when(mockRepo.findById(-1L))
.thenThrow(new IllegalArgumentException("ID 不能为负数"));
// 对 void 方法设定行为
doThrow(new RuntimeException("数据库连接失败"))
.when(mockRepo).deleteById(anyLong());
doNothing().when(mockRepo).deleteById(anyLong()); // 什么都不做(这是 void mock 的默认行为)Spy 的本质:包装真实对象
这是很多人搞混的地方。
spy(realObject) 创建的对象,是对真实对象的包装,它:
- 默认执行真实方法——不设 stub 的话,会调用真实实现
- 记录所有方法调用——和 Mock 一样,调用都被记录
- 可以部分 stub——可以选择性地拦截某些方法,其余走真实逻辑
@Test
void testSpyBehavior() {
List<String> realList = new ArrayList<>();
List<String> spyList = spy(realList);
// 没有 stub 的方法走真实逻辑
spyList.add("Hello");
spyList.add("World");
assertEquals(2, spyList.size()); // 真实的 size() 返回 2
// 可以 stub 部分方法
doReturn(100).when(spyList).size();
assertEquals(100, spyList.size()); // 现在返回 stub 值了
// 但 add 操作还是真实的
spyList.add("Third");
// size() 仍然返回 100(被 stub 了),但真实 list 里有 3 个元素
}什么时候用 Spy
Spy 主要用于以下场景:
场景一:测试一个真实类,但需要 mock 掉其中一个内部依赖
@Test
void testOrderService_withSpyOnEmailSender() {
// OrderService 的真实实现,但 emailSender 被替换
OrderService realOrderService = new OrderService(realInventoryService, mockPaymentService);
OrderService spyOrderService = spy(realOrderService);
// 假设 OrderService 里有一个 sendConfirmationEmail 方法,我们不想真的发邮件
doNothing().when(spyOrderService).sendConfirmationEmail(any());
Order order = spyOrderService.createOrder(validRequest);
assertNotNull(order.getId()); // 真实的 createOrder 逻辑
verify(spyOrderService).sendConfirmationEmail(any()); // 验证它被调用了
}场景二:验证一个方法的内部调用关系
@Test
void testCache_shouldNotCallDatabaseWhenCacheHit() {
CachedUserService spyService = spy(new CachedUserService(userRepository, cacheManager));
// 第一次调用(缓存未命中,应该查数据库)
spyService.getUserById(1L);
verify(spyService, times(1)).loadFromDatabase(1L);
// 第二次调用(缓存命中,不应该再查数据库)
spyService.getUserById(1L);
verify(spyService, times(1)).loadFromDatabase(1L); // 仍然是1次,没有增加
}ArgumentCaptor:捕获参数做断言
ArgumentCaptor 解决的问题是:当被测方法把参数传给 mock 对象时,你想验证这个参数的具体内容。
@Test
void testCreateOrder_shouldSendCorrectNotification() {
// Arrange
NotificationService mockNotification = mock(NotificationService.class);
OrderService orderService = new OrderService(mockNotification, orderRepo);
// Act
Order order = orderService.createOrder(new CreateOrderRequest(userId, productId, 2));
// Assert:验证通知被发送,且内容正确
ArgumentCaptor<String> emailCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class);
verify(mockNotification).sendEmail(emailCaptor.capture(), messageCaptor.capture());
assertEquals("user@example.com", emailCaptor.getValue());
assertTrue(messageCaptor.getValue().contains(order.getOrderNo()),
"通知邮件应该包含订单号");
assertTrue(messageCaptor.getValue().contains("下单成功"),
"通知邮件应该包含成功提示");
}捕获复杂对象
@Test
void testUserRegistration_shouldSaveCorrectUserData() {
// Arrange
UserRepository mockRepo = mock(UserRepository.class);
when(mockRepo.save(any())).thenAnswer(inv -> inv.getArgument(0));
UserService userService = new UserService(mockRepo, passwordEncoder);
// Act
userService.register("zhangsan", "password123", "zhangsan@test.com");
// 捕获 save 方法的参数
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
verify(mockRepo).save(userCaptor.capture());
User savedUser = userCaptor.getValue();
assertEquals("zhangsan", savedUser.getUsername());
assertEquals("zhangsan@test.com", savedUser.getEmail());
assertNotEquals("password123", savedUser.getPassword(), "密码应该被加密存储");
assertTrue(passwordEncoder.matches("password123", savedUser.getPassword()),
"密码应该可以通过 BCrypt 验证");
assertNotNull(savedUser.getCreateTime());
assertEquals(UserStatus.ACTIVE, savedUser.getStatus());
}踩坑实录三则
踩坑一:用 when() 对 void 方法 stub,编译报错
现象:
when(mockService.doSomethingVoid()).thenReturn(null); // 编译错误!原因:when() 内部调用了 mock 方法,对于 void 方法,这没有返回值,语法上不合法。
解法:对 void 方法使用 doXxx().when() 风格:
doNothing().when(mockService).doSomethingVoid();
doThrow(new RuntimeException()).when(mockService).doSomethingVoid();
doAnswer(invocation -> {
System.out.println("void 方法被调用了");
return null;
}).when(mockService).doSomethingVoid();踩坑二:Spy 对象里用了 when() 导致真实方法被调用
现象:
List<String> spyList = spy(new ArrayList<>());
when(spyList.get(0)).thenReturn("mocked"); // 这里抛 IndexOutOfBoundsException!原因:when(spyList.get(0)) 会先真实调用 spyList.get(0),而 ArrayList 是空的,所以抛异常。
解法:对 Spy 对象的 stub 一律使用 doReturn().when() 风格,避免真实调用:
doReturn("mocked").when(spyList).get(0); // 安全,不会触发真实方法踩坑三:verify 的参数匹配器混用导致 InvalidUseOfMatchersException
现象:
// 运行时抛 InvalidUseOfMatchersException
verify(mockService).doSomething(anyString(), "具体值");原因:Mockito 规定,在一次方法调用的参数里,要么全部用参数匹配器(any()、eq()、anyString() 等),要么全部用具体值。不能混用。
解法:把具体值也用 eq() 包一下:
verify(mockService).doSomething(anyString(), eq("具体值")); // 正确使用 @Mock 和 @InjectMocks 注解
手动 mock() 和注入有点繁琐,Mockito 提供了注解方式:
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private PaymentService paymentService;
@Mock
private NotificationService notificationService;
@InjectMocks
private OrderService orderService; // 自动注入上面的 Mock
@Captor
private ArgumentCaptor<Order> orderCaptor;
@Test
void testCreateOrder() {
when(paymentService.charge(anyLong(), any())).thenReturn(PaymentResult.success("txn001"));
when(orderRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
Order order = orderService.createOrder(buildRequest());
verify(orderRepository).save(orderCaptor.capture());
assertEquals(OrderStatus.PAID, orderCaptor.getValue().getStatus());
}
}@InjectMocks 的注入策略:优先构造器注入,其次属性注入,最后 setter 注入。如果构造器参数和 @Mock 类型不匹配,会静默失败(这是 @InjectMocks 的著名陷阱,下一篇专门讲)。
一个判断标准
到底什么时候用 Mock,什么时候用 Spy?
我的判断标准很简单:
用 Mock:当你要替换掉一个外部依赖(数据库、HTTP、消息队列),不想让测试真正调用它时,用 Mock。
用 Spy:当你想测试一个真实类的大部分逻辑,但需要拦截其中的某个具体方法时,用 Spy。
如果你发现自己对一个 Mock 对象 stub 了超过 5 个方法,很可能是被测代码的设计有问题——依赖太多,职责不清。这个时候不是要写更复杂的测试,而是要重构代码。
