Mockito 的 verify 怎么验证方法调用?ArgumentCaptor 和参数匹配器怎么用?
简化版
除了「打桩(stubbing,when(...).thenReturn(...) 设定 mock 的返回值)」,Mockito 还有一大能力是「验证交互(verify)」——检查「某个 mock 的方法被调用了没、调用了几次、用什么参数调的」。核心:① verify(mock).方法(参数)——验证这个方法被调用了(默认 1 次);② 验证次数——verify(mock, times(2))(2 次)、never()(没调用)、atLeast(n)/atMost(n)(至少/至多);③ 参数匹配器(argument matcher)——验证/打桩时不写死参数,用 any()、eq(x)、anyString()、argThat(条件) 等灵活匹配(如 verify(mock).save(any(User.class)) 验证「用任意 User 调过 save」);④ ArgumentCaptor(参数捕获器)——「捕获」方法被调用时实际传入的参数,然后对捕获到的参数做断言(比 matcher 更精细,能拿到实际对象校验它的字段)。为什么需要 verify:有些方法没有返回值(如 save、send、log),没法用返回值断言,只能验证「它被以正确的参数调用了」(交互验证)。核心:verify 验证方法调用(次数、参数),参数匹配器灵活匹配参数,ArgumentCaptor 捕获实际参数做精细断言。
详细版
verify 和参数处理:
| 工具 | 作用 |
|---|---|
| verify(mock).method() | 验证调用(默认 1 次) |
| verify(mock, times(n)) | 验证调用 n 次 |
| verify(mock, never()) | 验证从未调用 |
| verify(mock, atLeast/atMost(n)) | 至少/至多 n 次 |
| any()/eq()/anyString() | 参数匹配器(灵活匹配) |
| argThat(条件) | 自定义参数匹配 |
| ArgumentCaptor | 捕获实际参数做断言 |
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock UserRepository userRepo;
@Mock EmailService emailService;
@InjectMocks OrderService orderService;
@Test void createOrder() {
// 打桩
when(userRepo.findById(1L)).thenReturn(new User("Tom"));
// 执行
orderService.create(1L);
// ① verify:验证 save 被调用了 1 次
verify(userRepo).save(any(Order.class));
// ② 验证次数
verify(emailService, times(1)).send(anyString(), anyString());
verify(emailService, never()).sendSms(anyString()); // 没调用短信
// ③ 参数匹配器 + 精确
verify(userRepo).save(argThat(order -> order.getUserId() == 1L));
// ④ ArgumentCaptor:捕获实际参数做断言
ArgumentCaptor<Order> captor = ArgumentCaptor.forClass(Order.class);
verify(userRepo).save(captor.capture());
Order savedOrder = captor.getValue(); // 拿到实际保存的 Order
assertEquals(1L, savedOrder.getUserId()); // 断言它的字段
assertEquals("PENDING", savedOrder.getStatus());
}
}
⚠️ 两个关键点:「没有返回值的方法只能用 verify 验证」和「ArgumentCaptor 比 matcher 更适合校验复杂参数」。为什么需要 verify:有返回值的方法可以用「打桩 + 断言返回值」测试(
when().thenReturn()+assertEquals);但没有返回值的方法(void save()、void send()、void log())没法用返回值断言——你怎么知道save被正确调用了?只能用verify验证「它被以什么参数、调用了几次」(交互验证)。ArgumentCaptor vs 参数匹配器:argThat(条件)能在 verify 时校验参数是否满足条件(内联),但如果参数是复杂对象、要校验多个字段,ArgumentCaptor更清晰——它「捕获」方法实际被调用时传入的参数对象,然后你可以像普通对象一样对它做多个断言(captor.getValue().getXxx()),失败信息也更清晰(能看到实际值)。参数匹配器的坑:一旦用了 matcher,所有参数都要用 matcher(不能一个用eq(1)、一个直接写"abc"),要么全 matcher(eq("abc"))、要么全不用;混用会报错。
完整版教学
一、Mockito 的两大能力:打桩与验证
先理解 Mockito 的两大能力:
Mockito 的两大能力:
① 打桩(Stubbing):设定 mock 的行为(返回值)
when(mock.method(参数)).thenReturn(值)
→ 让 mock 返回你要的值(控制测试的输入/依赖)
② 验证(Verify):检查 mock 的交互
verify(mock).method(参数)
→ 检查方法被调用了没、几次、什么参数(验证行为)
两者的角色:
打桩:控制"依赖返回什么"(给被测代码喂数据)
验证:检查"被测代码怎么用依赖"(验证交互是否正确)
例(下单服务):
打桩:when(userRepo.findById(1)).thenReturn(user)
→ 让 userRepo 返回一个用户(控制依赖)
验证:verify(userRepo).save(any())
→ 检查 orderService 调了 save(验证行为)
为什么两者都需要:
打桩:让被测代码在"可控的依赖行为"下运行
验证:确认被测代码"正确地调用了依赖"(尤其无返回值的)
所以 Mockito 打桩(设返回值)+验证(检查交互),两大能力
Mockito 的两大能力:① 打桩(Stubbing)设定 mock 行为(when(mock.method()).thenReturn(值) 让 mock 返回要的值、控制测试输入/依赖)、② 验证(Verify)检查 mock 交互(verify(mock).method() 检查方法被调用了没/几次/什么参数)。角色:打桩控制依赖返回什么、验证检查被测代码怎么用依赖。为什么都需要:打桩让被测代码在可控依赖下运行、验证确认正确调用依赖(尤其无返回值的)。理解「Mockito 两大能力:打桩(when.thenReturn 设返回值控制依赖)+验证(verify 检查调用没/几次/什么参数);打桩控制依赖返回、验证检查被测代码怎么用依赖」,就理解了 Mockito 的两大能力。
二、verify:验证方法调用
verify——验证方法被调用:
verify:验证 mock 的方法被调用
verify(mock).method(参数)
→ 验证这个方法被调用了(默认恰好 1 次)
→ 没调用/参数不对 → 验证失败
验证次数:
verify(mock, times(n)):恰好 n 次
verify(mock, never()):从未调用(0 次)
verify(mock, atLeastOnce()):至少 1 次
verify(mock, atLeast(n)):至少 n 次
verify(mock, atMost(n)):至多 n 次
verify(mock, times(0)):0 次(同 never)
例:
verify(userRepo).save(order); // save 被调 1 次
verify(emailService, times(2)).send(...); // send 被调 2 次
verify(smsService, never()).send(...); // sms 从未调用
其他验证:
verifyNoInteractions(mock):mock 没有任何交互
verifyNoMoreInteractions(mock):验证后没有更多交互
InOrder:验证调用顺序
InOrder inOrder = inOrder(mock1, mock2);
inOrder.verify(mock1).a();
inOrder.verify(mock2).b(); // 验证 a 在 b 之前调
所以 verify 验证方法调用(次数:times/never/atLeast/atMost)
verify 验证方法被调用——verify(mock).method(参数)(验证被调用,默认恰好 1 次,没调用/参数不对失败)。验证次数:times(n)(恰好 n 次)、never()(0 次)、atLeastOnce()/atLeast(n)/atMost(n)。其他:verifyNoInteractions(没任何交互)、verifyNoMoreInteractions(没更多交互)、InOrder(验证调用顺序)。理解「verify 验证方法调用(默认 1 次,没调用/参数不对失败);次数 times(n)/never/atLeastOnce/atLeast/atMost;其他 verifyNoInteractions/InOrder 验证顺序」,就掌握了 verify。
三、为什么需要 verify
理解「为什么需要 verify」——无返回值的方法:
有返回值的方法:用返回值断言就行
int result = calc.add(2, 3);
assertEquals(5, result); // 用返回值断言
→ 不需要 verify
无返回值的方法(void):没返回值,怎么测?
orderService.create(1L); // create 内部会调 save、send(都无返回值)
→ 怎么验证 create 做对了?
→ 它调了 userRepo.save(order)、emailService.send(...) 吗?
用 verify 验证交互:
verify(userRepo).save(any()); // 验证调了 save
verify(emailService).send(...); // 验证调了 send
→ 通过"验证依赖被正确调用"来测试无返回值的方法
典型的无返回值方法:
save/update/delete(持久化)
send/publish(发送消息、邮件)
log/record(日志、记录)
→ 这些"做了什么"没法用返回值看,只能 verify
verify 验证的是"交互/行为":
不是验证"返回了什么"(值)
而是验证"调用了什么依赖、怎么调的"(交互)
→ 行为验证(Behavior Verification)
所以 verify 用于验证无返回值方法(交互验证,save/send/log)
「为什么需要 verify」——有返回值的方法用返回值断言(assertEquals(5, result) 不需要 verify)。无返回值的方法(void)没返回值怎么测——orderService.create(1L) 内部调 save、send(都无返回值),怎么验证做对了?用 verify 验证交互(verify(userRepo).save(any()) 验证调了 save)。典型无返回值方法:save/update/delete(持久化)、send/publish(发消息邮件)、log(日志)。verify 验证的是交互/行为(调用了什么依赖、怎么调的)而非返回值。理解「有返回值用返回值断言不需 verify、无返回值方法(save/send/log)没返回值只能 verify 验证交互(验证调了什么依赖怎么调的);verify 是行为验证不是值验证」,就理解了为什么需要 verify。
四、参数匹配器
参数匹配器(argument matcher)——灵活匹配参数:
参数匹配器:打桩/验证时不写死参数,灵活匹配
常用匹配器:
any():任意参数(包括 null)
any(Xxx.class):任意某类型
anyString()/anyInt()/anyLong():任意某基本类型/字符串
eq(value):等于某值(用 matcher 时表示精确等于)
isNull()/notNull():null/非 null
contains("x")/startsWith("x"):字符串匹配
argThat(条件):自定义条件(Lambda)
用在打桩:
when(userRepo.findById(anyLong())).thenReturn(user);
→ 任意 id 都返回这个 user
用在验证:
verify(userRepo).save(any(Order.class)); // 用任意 Order 调过 save
verify(emailService).send(eq("tom@x.com"), anyString()); // 收件人精确、内容任意
argThat(自定义匹配):
verify(userRepo).save(argThat(order ->
order.getUserId() == 1L && order.getStatus().equals("PENDING")));
→ 验证 save 的参数满足这个条件
★ 匹配器的坑(不能混用):
用了 matcher,所有参数都要用 matcher
✗ verify(mock).send(eq("a"), "b") // 一个 matcher 一个字面量 → 报错
✓ verify(mock).send(eq("a"), eq("b")) // 都用 matcher
✓ verify(mock).send("a", "b") // 都不用
→ 要么全 matcher、要么全字面量
所以参数匹配器灵活匹配参数(any/eq/anyString/argThat),不能混用
参数匹配器 灵活匹配参数——常用:any()(任意,含 null)、any(Xxx.class)、anyString/anyInt、eq(value)(精确等于)、isNull/notNull、contains/startsWith、argThat(条件)(自定义 Lambda)。用在打桩(when(findById(anyLong())).thenReturn(user))和验证(verify().save(any(Order.class)))。argThat 自定义匹配(校验参数满足条件)。匹配器的坑(不能混用):用了 matcher 所有参数都要用 matcher(send(eq("a"), "b") 报错、要 send(eq("a"), eq("b")) 或 send("a","b"))。理解「参数匹配器灵活匹配:any/any(类)/anyString/eq(精确)/argThat(自定义);用在打桩和验证;★不能混用(用了 matcher 所有参数都要 matcher)」,就掌握了参数匹配器。
五、ArgumentCaptor:捕获参数
ArgumentCaptor——捕获实际参数做精细断言:
ArgumentCaptor(参数捕获器):
"捕获"方法实际被调用时传入的参数,然后对它做断言
用法:
① 创建 Captor:
ArgumentCaptor<Order> captor = ArgumentCaptor.forClass(Order.class);
② verify 时用 captor.capture() 捕获:
verify(userRepo).save(captor.capture());
③ 拿到捕获的参数、断言:
Order savedOrder = captor.getValue();
assertEquals(1L, savedOrder.getUserId());
assertEquals("PENDING", savedOrder.getStatus());
→ 拿到实际保存的 Order 对象,校验它的多个字段
多次调用(捕获多个):
captor.getAllValues():所有捕获的值(List)
ArgumentCaptor vs argThat(参数匹配器):
argThat(条件):在 verify 时内联校验参数满足条件
verify(mock).save(argThat(o -> o.getUserId() == 1L))
→ 简单条件、内联
ArgumentCaptor:捕获参数,之后做多个断言
→ 复杂对象、要校验多个字段、失败信息清晰
什么时候用 ArgumentCaptor:
① 参数是复杂对象、要校验多个字段
② 想对参数做详细断言(比 argThat 清晰)
③ 失败时想看到实际传入的值(更好定位)
@Captor 注解(简化创建):
@Captor ArgumentCaptor<Order> captor;
→ 不用手动 forClass
所以 ArgumentCaptor 捕获实际参数做精细断言(复杂对象、多字段校验)
ArgumentCaptor 捕获实际参数做精细断言——① 创建 Captor(forClass)→ ② verify 时 captor.capture() 捕获 → ③ captor.getValue() 拿到参数做断言(拿到实际保存的对象校验多个字段)。多次调用用 getAllValues()。ArgumentCaptor vs argThat:argThat 内联校验简单条件、ArgumentCaptor 捕获后做多个断言(复杂对象、多字段校验、失败信息清晰)。什么时候用 ArgumentCaptor:参数是复杂对象要校验多个字段、想详细断言、失败想看实际值。@Captor 注解简化创建。理解「ArgumentCaptor 捕获实际参数做断言:forClass 创建→capture()捕获→getValue()拿参数断言(校验多字段);vs argThat:argThat 内联简单条件、Captor 复杂对象多字段清晰;@Captor 简化」,就掌握了 ArgumentCaptor。
六、实践与总结
总结 verify 和参数处理的实践:
实践建议:
① 无返回值方法用 verify 验证交互(save/send/log)
② 有返回值方法用返回值断言(不用 verify)
③ 验证次数(times/never/atLeast)确认调用符合预期
④ 参数不关心用 any()、关心用 eq()、复杂用 ArgumentCaptor
⑤ 校验复杂对象的多个字段用 ArgumentCaptor(比 argThat 清晰)
常见验证场景:
① 验证依赖被正确调用(save 被调、用对的参数)
② 验证没有多余调用(never——不该调的没调)
③ 验证调用次数(times——调了正确的次数)
④ 验证参数正确(ArgumentCaptor——传入的对象字段对)
⑤ 验证调用顺序(InOrder)
注意点:
① 参数匹配器不能混用(要么全 matcher、要么全字面量)
② 别过度验证(验证关键交互即可,别验证所有细节 → 脆弱)
③ verify 验证交互、断言验证状态/值 → 结合用
核心总结:
verify 验证方法调用(次数、参数)——尤其无返回值方法
参数匹配器(any/eq/anyString/argThat)灵活匹配参数
ArgumentCaptor 捕获实际参数做精细断言(复杂对象、多字段)
打桩(when.thenReturn)控制依赖 + 验证(verify)检查交互
verify 和参数处理的实践:无返回值方法用 verify、有返回值用返回值断言、验证次数确认调用、参数不关心用 any 关心用 eq 复杂用 ArgumentCaptor、复杂对象多字段用 ArgumentCaptor。场景:验证依赖被正确调用、验证没多余调用(never)、验证次数(times)、验证参数(Captor)、验证顺序(InOrder)。注意:参数匹配器不能混用、别过度验证(脆弱)、verify 验证交互+断言验证状态结合。理解「实践:无返回值用 verify/有返回值用断言/验证次数/参数 any-eq-Captor/复杂对象用 Captor;场景验证调用/没多余调用/次数/参数/顺序;注意匹配器不混用+别过度验证」,就掌握了实践与总结。
记忆钩子:「Mockito 两大能力:打桩(when.thenReturn 设返回值控制依赖)+★验证(verify 检查交互);verify(mock).method(参数)验证调用(默认 1 次);次数:times(n)/never/atLeastOnce/atLeast/atMost;★为什么需要 verify:无返回值方法(save/send/log)没法用返回值断言,只能验证被以什么参数调了几次(交互验证);参数匹配器灵活匹配:any()/any(类)/anyString/eq(精确)/argThat(自定义 Lambda),★不能混用(用了 matcher 所有参数都要 matcher);ArgumentCaptor 捕获实际参数做精细断言:forClass 创建→capture()→getValue()拿参数校验多字段(复杂对象比 argThat 清晰),@Captor 简化;InOrder 验证调用顺序;别过度验证(脆弱)」。
七、常见误区与追问
- 误区:无返回值的方法没法测试。 能测——用 verify 验证交互:虽然没有返回值可断言,但可以验证「它被以正确的参数调用了、调用了正确的次数」;比如 orderService.create() 内部调 userRepo.save(),就 verify(userRepo).save(argThat(…)) 验证 save 被以正确的 Order 调用;这是行为验证。
- 误区:参数匹配器可以和字面量混用。 不能——一旦某个参数用了 matcher(如 eq、any),该方法的所有参数都要用 matcher;verify(mock).send(eq(“a”), “b”) 会报错(一个 matcher 一个字面量);要么全用 matcher(send(eq(“a”), eq(“b”)))、要么全用字面量(send(“a”, “b”))。
- 误区:验证复杂参数用 argThat 就够了。 argThat 适合简单条件的内联校验;但如果参数是复杂对象、要校验多个字段,用 ArgumentCaptor 更清晰——它捕获方法实际被调用时传入的参数对象,然后你可以像普通对象一样对它做多个断言(captor.getValue().getXxx()),失败信息也能看到实际值,更好定位。
- 误区:verify 和 when(打桩)是一回事。 不同——when(…).thenReturn(…) 是打桩(设定 mock 被调用时返回什么,控制依赖的行为,测试前设置);verify(mock).method() 是验证(检查 mock 的方法被调用了没、几次、什么参数,测试后检查交互);打桩控制输入、验证检查行为。
- 追问:Mockito 的 verify 是干什么的,什么时候用? verify 用于验证 mock 的方法被调用的情况——检查某个方法被调用了没、调用了几次(times/never/atLeast/atMost)、用什么参数调的;主要用于测试没有返回值的方法(void,如 save、send、log):这些方法没法用返回值断言,只能通过 verify 验证「它被以正确的参数、正确的次数调用了」(交互/行为验证);比如验证下单服务确实调用了 userRepo.save() 和 emailService.send()。
- 追问:ArgumentCaptor 和参数匹配器 argThat 有什么区别? argThat(条件) 在 verify 时内联校验参数是否满足条件(用 Lambda),适合简单条件;ArgumentCaptor 是「捕获」方法实际被调用时传入的参数对象,之后可以像普通对象一样对它做多个断言(captor.getValue()),适合参数是复杂对象、要校验多个字段的场景;ArgumentCaptor 的失败信息更清晰(能看到实际传入的值);简单条件用 argThat、复杂对象多字段校验用 ArgumentCaptor。
- 追问:怎么用 ArgumentCaptor 校验保存的对象的字段? ① 创建 captor:ArgumentCaptor
captor = ArgumentCaptor.forClass(Order.class)(或用 @Captor 注解);② 在 verify 时用 captor.capture() 捕获参数:verify(userRepo).save(captor.capture());③ 用 captor.getValue() 拿到实际被保存的 Order 对象,然后对它做断言:assertEquals(1L, captor.getValue().getUserId())、assertEquals(“PENDING”, captor.getValue().getStatus());如果方法被调用多次,用 captor.getAllValues() 拿到所有捕获的值(List)。
八、加强记忆
Mockito 除了「打桩(stubbing,when(...).thenReturn(...) 设 mock 返回值)」,还有「验证交互(verify)」——检查某个 mock 的方法被调用了没、几次、用什么参数调的。① verify(mock).method(参数) 验证方法被调用(默认 1 次);② 验证次数:times(n)(恰好 n 次)、never()(0 次)、atLeast(n)/atMost(n);③ 参数匹配器:any()(任意)、any(Xxx.class)、anyString()、eq(value)(精确)、argThat(条件)(自定义 Lambda)——一旦用 matcher,所有参数都要用 matcher(不能混用);④ ArgumentCaptor(参数捕获器):forClass 创建 → capture() 捕获 → getValue() 拿到实际传入的参数对象做多个断言(复杂对象、多字段校验,比 argThat 清晰、失败信息能看实际值,@Captor 简化)。为什么需要 verify:没有返回值的方法(save/send/log)没法用返回值断言,只能验证它被以正确的参数、正确的次数调用了(交互/行为验证)。打桩控制依赖返回什么、验证检查被测代码怎么用依赖。注意别过度验证(脆弱)。一句话「Mockito verify 验证方法调用(次数 times/never/atLeast、参数);为什么需要:无返回值方法(save/send/log)没法用返回值断言只能验证交互;参数匹配器 any/eq/anyString/argThat 灵活匹配(不能混用);ArgumentCaptor 捕获实际参数做精细断言(复杂对象多字段,比 argThat 清晰);打桩控制依赖+验证检查交互」。