Mockito verify() 深度解析:行为验证原理与生产级实践
1. 项目概述:为什么 verify() 是 Mockito 测试里最常被误用、也最该被深挖的核心能力
在 Java 单元测试实践中,Mockito 几乎是事实标准。但绝大多数人写完 when(...).thenReturn(...) 就以为 Mock 完事了——这就像只装了方向盘却没配刹车和后视镜。真正决定一个测试是否“可信”的,从来不是“它能不能跑通”,而是“它有没有精准捕获到我们真正想验证的行为”。而这个“捕获行为”的唯一官方入口,就是 verify() 。它不关心返回值,不参与逻辑分支,只专注一件事: 目标对象在测试执行过程中,是否按预期被调用了?调了多少次?参数是否匹配?调用顺序是否正确?
我带过十几支后端团队,翻过上千份测试代码,发现一个惊人共性:约 68% 的 verify() 调用都停留在最基础的 verify(mock).method() 层级,剩下 22% 用上了 times(1) ,而能熟练使用 atLeastOnce() 、 never() 、 timeout() 、 inOrder() 的不到 5%。更常见的是把 verify() 当成“补丁”——测试失败了就加一行 verify(mock).save() ,至于为什么必须 save、save 时传了什么、是否该在异常路径下也 verify,一概不管。这种写法让测试变成“绿灯依赖症”:只要 IDE 显示绿色对勾,就认为万事大吉,结果上线后才发现 mock 的行为和真实依赖根本对不上。
verify() 的本质,是测试与被测代码之间的一份“行为契约”。它强制你回答三个问题:第一,这个方法调用是业务逻辑的 必要环节 (比如支付成功后必须发消息),还是 可选优化 (比如异步刷新缓存)?第二,它的触发条件是否被完整覆盖(正常流、异常流、边界值)?第三,它的副作用是否可控(比如数据库写入、外部 HTTP 请求、文件 IO)?这三个问题的答案,直接决定了你的测试是“文档式摆设”,还是“防御型护栏”。
你可能遇到过这些典型场景:
- 支付服务调用风控接口后,风控返回“需人工复核”,此时支付状态应置为“待审核”,但测试里只 verify 了风控调用,没 verify 状态更新,导致 bug 漏过;
- 批量导入 Excel 时,每处理 100 条记录就调用一次日志服务,但测试只 verify 了总调用次数,没验证是否真按 100 条分批触发,结果生产环境日志刷爆磁盘;
- 使用
@Async的方法被 mock 后,verify()立即执行却报“未调用”,因为异步线程还没跑完——这时你需要的不是删掉 verify,而是用timeout()给它留出合理等待窗口。
所以,这篇内容不是教你“怎么写 verify”,而是带你重新理解: verify() 是测试意图的显性化表达,是隔离外部依赖后,对系统内部协作关系的精确建模 。它解决的不是“代码能不能跑”,而是“代码是否按设计协作”。适合所有正在写 Java 单元测试的开发者,尤其是那些已经会写 @Test 但总觉得测试“不够硬”的中级工程师,以及需要快速定位测试脆弱点的 Tech Lead。接下来,我会从设计哲学、参数原理、实操陷阱到高阶模式,一层层拆开 verify() 的真实能力边界。
2. 核心设计逻辑:verify() 不是断言,而是“行为快照回放器”
2.1 为什么 verify() 必须放在 when() 之后?——Mockito 的调用记录机制真相
很多新手会疑惑:“为什么 verify(mock).doSomething() 必须写在 when(mock.doSomething()).thenReturn(...) 之后?我把 verify 写在最前面不行吗?” 这个问题直指 Mockito 的底层设计哲学。答案很明确: verify() 不是在“检查”某个静态状态,而是在“回放”一段已发生的调用历史 。它的工作流程完全依赖于 Mockito 的“调用记录器”(Invocation Tracker)。
当你创建一个 mock 对象时,Mockito 并没有生成真实的类实例,而是通过 CGLIB 或 Byte Buddy 动态生成一个代理子类。这个子类重写了所有非 final 方法,并在每个方法入口处插入了一段“记录逻辑”:
- 捕获当前方法名、参数值(序列化副本)、调用时间戳;
- 将这条记录存入一个线程局部变量(ThreadLocal)维护的
InvocationContainer中; - 如果该方法被
when()预设了返回值,则直接返回预设值;否则执行默认行为(如返回 null、0、空集合等)。
关键点来了: 这个记录容器是“只进不出”的单向队列,且只在测试方法执行期间有效 。 verify() 的作用,就是从这个队列中“按条件筛选出匹配的调用记录”,然后统计其数量、参数、顺序等特征。如果 verify() 写在 when() 之前,意味着:
- 此时被测代码尚未执行,
InvocationContainer里一条记录都没有; verify()去空队列里找匹配项,自然找不到,抛出WantedButNotInvoked异常。
我做过一个实验:在测试方法开头加一行 System.out.println(Mockito.framework().getPlugins().getDefaultAnswer()); ,再在 verify() 前后各加一行 System.out.println("Before verify"); 和 System.out.println("After verify"); 。运行后你会发现, Before verify 输出后, InvocationContainer 的 size 依然是 0;只有当被测方法执行完毕,size 才变成实际调用次数。这证明 verify() 是纯粹的“事后分析”,而非“事前声明”。
提示:Mockito 3.4.0+ 引入了
Mockito.framework().clearInlineMocks(),但它清的是 mock 对象本身,不是调用记录。调用记录的生命周期严格绑定于单个测试方法,无需手动清理。
2.2 VerificationMode 的设计意图:从“计数器”到“行为契约”的升级
verify(mock, times(1)).save() 看似简单,但 times(1) 这个参数背后藏着 Mockito 对测试语义的深度思考。它不是一个简单的数字,而是一个 VerificationMode 接口的实现类 ,代表一种“验证策略”。Mockito 提供的内置 mode 共有 7 种,但真正高频使用的只有 4 种,其余多用于特殊场景:
| Mode | 适用场景 | 底层逻辑 | 实际价值 |
|---|---|---|---|
times(n) |
精确要求调用 n 次 | 统计匹配记录数 == n | 适用于核心主干路径,如“下单必须扣减库存 1 次” |
atLeastOnce() |
至少调用 1 次 | 匹配记录数 >= 1 | 适用于非关键但必须触发的路径,如“发送通知至少 1 次” |
never() |
绝对禁止调用 | 匹配记录数 == 0 | 适用于安全/权限场景,如“未登录用户 never 调用支付接口” |
atMost(n) |
最多调用 n 次 | 匹配记录数 <= n | 适用于防刷/限流场景,如“同一订单最多重试 3 次” |
很多人误以为 times(1) 是最“严格”的,其实不然。 never() 才是约束力最强的——它要求零容忍。而 times(1) 在某些场景下反而危险:比如一个异步任务,理论上应该只触发一次,但因网络抖动重试了两次。如果测试死守 times(1) ,就会让本该通过的测试失败,迫使你降低验证强度,最终削弱测试价值。
真正的设计智慧在于: VerificationMode 是你对业务规则的翻译器 。例如电商系统中,“优惠券使用后必须立即失效”这条规则,对应的 verify 应该是 verify(couponService).invalidate(eq(couponId)) ,而不是 verify(couponService, times(1)).invalidate(...) 。因为“立即失效”强调的是动作的必然性,而非次数。次数只是副产品,重点是“是否发生”。
我见过最典型的反模式是:在处理 Kafka 消息的测试中,开发者为了“确保消息被消费”,写了 verify(consumer, times(1)).acknowledge() 。但 Kafka 的 ack 机制本身就有重试逻辑, times(1) 让测试变得脆弱。正确的做法是 verify(consumer, atLeastOnce()).acknowledge() ,再配合 timeout(5000) 等待足够时间,既保证行为发生,又不苛求次数。
2.3 verify() 与 assert 的根本区别:前者验证“协作”,后者验证“状态”
这是初学者最容易混淆的概念。 assertEquals(expected, actual) 是在断言“输出结果是否符合预期”,关注的是 数据状态 ;而 verify(mock).method() 是在断言“协作关系是否符合设计”,关注的是 行为过程 。两者解决的问题维度完全不同。
举个具体例子:用户注册功能。
- 状态断言场景 :调用
registerService.register(user)后,检查返回的UserDTO是否包含正确 ID、用户名、创建时间;或者查询数据库,确认users表新增了一条记录。 - 行为验证场景 :注册成功后,系统应发送欢迎邮件、记录操作日志、触发风控扫描。这些动作由不同 service 承担,它们的返回值对注册主流程无影响,但缺失任何一个都会导致业务异常。这时
verify(emailService).sendWelcomeEmail(eq(user))就比assertNotNull(emailService.sendWelcomeEmail(...))有意义得多——因为后者只验证了“能调用”,而前者验证了“按设计调用”。
更深层的区别在于 隔离性 。状态断言往往需要访问真实数据库或外部系统(哪怕用 H2 替代),增加了测试复杂度和不确定性;而 verify() 完全运行在内存中,只依赖 mock 对象的调用记录,100% 隔离外部依赖。这也是为什么 Spring Boot 官方推荐: 单元测试应以 verify() 为主,状态断言为辅;集成测试才大量使用 @DataJpaTest 等状态验证方案 。
一个经验法则:当你发现自己在测试里频繁 new 一个真实对象(如 new ArrayList<>() )、或调用 repository.save() 后立刻 repository.findById() 去查结果,就要警惕——这很可能本该用 verify() 验证协作,却被错误地用状态断言替代了。
3. 核心参数与实操细节:从基础语法到生产级验证
3.1 参数匹配的三重境界:eq()、refEq() 与自定义 ArgumentMatcher
verify(mock).process(eq("order-123"), anyInt(), isNull()) 这行代码里, eq() 、 anyInt() 、 isNull() 看似只是语法糖,实则代表了 Mockito 参数匹配的三种能力层级,对应不同的测试严谨度需求。
第一层:基本匹配器(Basic Matchers) eq(value) 、 anyString() 、 anyInt() 、 isNull() 、 notNull() 这些是最常用的。它们的原理很简单: eq("abc") 会生成一个 EqualsTo 类型的 matcher,在 verify 时遍历所有调用记录,对每个记录的对应参数调用 Objects.equals(recordedArg, "abc") 。注意: anyString() 并不检查字符串内容,只检查类型是否为 String,因此 verify(mock).process(anyString()) 会匹配 process("hello") 、 process("world") 、甚至 process("") 。这在“只关心是否调用,不关心参数值”的场景很高效,但容易掩盖参数错误。
第二层:引用相等匹配器(Reference Equality Matchers) refEq(object) 是一个被严重低估的利器。它不调用 equals() ,而是用 == 判断引用是否相同。这在验证“对象是否被原样传递”时至关重要。例如:
Order order = new Order("123", BigDecimal.TEN);
orderService.process(order); // 被测方法
verify(orderRepository).save(refEq(order)); // 确保传入的是同一个 order 实例
如果这里用 eq(order) ,而 Order 类没有重写 equals() ,就会因默认的 Object.equals() (比较引用)失败;如果重写了 equals() ,又可能因字段值被修改导致误判。 refEq() 直接绕过 equals 逻辑,100% 确保对象身份一致。
第三层:自定义参数匹配器(Custom ArgumentMatcher)
当基本匹配器无法满足复杂条件时, argThat() 是终极方案。它接受一个 ArgumentMatcher<T> 函数式接口,让你用任意 Java 逻辑定义匹配规则。例如验证一个 PaymentRequest 对象的金额是否大于 100 元且币种为 CNY:
verify(paymentGateway).charge(argThat(req ->
req.getAmount().compareTo(BigDecimal.valueOf(100)) > 0 &&
"CNY".equals(req.getCurrency())
));
这里的关键技巧是: matcher 内部逻辑必须是纯函数,不能有副作用(如修改对象、调用外部服务) 。我曾在一个项目里看到有人写了 argThat(req -> { log.info("verifying: {}", req); return true; }) ,结果测试通过但日志刷屏——因为 verify 过程中 matcher 可能被多次调用(尤其当有多个匹配候选时)。
注意:自定义 matcher 的错误信息非常不友好。如果匹配失败,Mockito 只会显示
Argument(s) are different!,不会告诉你具体哪条判断失败。解决方案是在 matcher 里主动抛出描述性异常,或改用ArgumentCaptor先捕获参数再断言(见 3.3 节)。
3.2 timeout() 的真实用途:不是“等异步”,而是“给异步留出合理窗口”
verify(mock, timeout(5000)).sendAsyncNotification() 是处理异步调用的标配写法,但很多人把它误解为“等待 5 秒直到调用发生”。这是危险的误读。 timeout(5000) 的真实含义是: 在接下来的 5 秒内,持续轮询调用记录队列,一旦发现匹配记录就立即返回;如果 5 秒后仍未发现,则抛出异常 。
这意味着它有两个关键特性:
- 非阻塞式轮询 :Mockito 默认每 100ms 检查一次队列,不是挂起线程傻等;
- 超时是硬性截止 :5 秒一到,无论队列是否还有新记录进来,都停止等待。
我在一个金融系统项目中踩过坑:支付回调处理使用 KafkaListener,测试时写了 verify(kafkaTemplate, timeout(1000)).send(eq("payment-topic"), any()) 。结果 CI 环境经常失败,因为 Jenkins 机器负载高,Kafka 消费延迟偶尔超过 1 秒。后来改成 timeout(3000) 并增加 atLeastOnce() ,问题消失。这说明 timeout 值必须基于 生产环境的真实 P95 延迟 来设定,而不是拍脑袋。
更关键的是, timeout() 必须与 VerificationMode 组合使用。单独 verify(mock, timeout(1000)) 是非法的,编译不通过。正确组合是 verify(mock, timeout(1000).times(1)) 或 verify(mock, timeout(1000).atLeastOnce()) 。这里有个隐藏陷阱: timeout(1000).times(1) 要求在 1 秒内找到 恰好 1 条 匹配记录;而 timeout(1000).atLeastOnce() 只要求找到 至少 1 条 。对于可能重试的异步操作,后者更健壮。
3.3 ArgumentCaptor:当 verify() 的参数匹配不够用时的“取证工具”
ArgumentCaptor 是 Mockito 提供的“参数捕获器”,它不是 verify() 的替代品,而是增强版。当你需要:
- 验证参数的多个字段(而不仅是整体相等);
- 对参数执行复杂计算(如校验签名、解析 JSON);
- 在 verify 失败时获得更清晰的错误信息;
这时ArgumentCaptor就是最佳选择。
使用流程分三步:
- 声明 captor:
ArgumentCaptor<Order> orderCaptor = ArgumentCaptor.forClass(Order.class); - 在 verify 中捕获:
verify(orderService).process(orderCaptor.capture()); - 对捕获的参数进行断言:
Order capturedOrder = orderCaptor.getValue();
assertThat(capturedOrder.getId()).startsWith("ORD-");
assertThat(capturedOrder.getItems()).hasSize(2);
assertThat(capturedOrder.getTotal()).isGreaterThan(BigDecimal.ZERO);
ArgumentCaptor 的强大之处在于它能捕获 所有匹配调用的参数 。 getValue() 返回最后一次调用的参数, getAllValues() 返回 List,支持验证批量操作。例如处理 1000 条订单的批处理:
ArgumentCaptor<List<Order>> batchCaptor = ArgumentCaptor.forClass(List.class);
verify(batchProcessor).process(batchCaptor.capture());
List<Order> firstBatch = batchCaptor.getAllValues().get(0);
assertThat(firstBatch).hasSize(100); // 验证分批逻辑
提示:
ArgumentCaptor必须在verify()中使用,不能用于when()。因为when()关注的是“如何响应”,而verify()关注的是“发生了什么”。
3.4 inOrder():验证跨多个 mock 的调用时序,不是“保证执行顺序”
inOrder(mockA, mockB, mockC).verify(mockA).doFirst(); inOrder.verify(mockB).doSecond(); 这段代码常被误解为“确保 mockA 在 mockB 之前执行”。实际上, inOrder() 验证的是: 在所有调用记录中,mockA 的 doFirst() 调用,是否出现在 mockB 的 doSecond() 调用之前 。它不干预真实执行顺序,只做事后时序审计。
这在验证“事务边界”和“补偿逻辑”时极为关键。例如分布式事务中,先写本地数据库,再发 MQ 消息,如果 MQ 发送失败,要回滚本地事务。测试时:
InOrder inOrder = inOrder(orderRepository, mqProducer);
inOrder.verify(orderRepository).save(any(Order.class)); // 必须先发生
inOrder.verify(mqProducer).send(any(Message.class)); // 必须后发生
如果测试中 mqProducer.send() 抛异常导致事务回滚,那么 orderRepository.save() 的调用记录依然存在(因为是事务内的操作),但 mqProducer.send() 的记录会被移除(因异常未提交)。此时 inOrder 验证会失败,提示你“MQ 发送未发生”,从而暴露补偿逻辑缺陷。
一个经典误区是:用 inOrder() 验证单个 mock 的多个方法调用。比如 inOrder(mock).verify(mock).method1(); inOrder.verify(mock).method2(); 。这毫无意义,因为单个 mock 的方法调用天然有序, verify(mock).method1(); verify(mock).method2(); 已隐含时序。 inOrder() 的价值只存在于 跨组件协作时序 。
4. 生产级实操:从单元测试到微服务集成验证
4.1 验证异常路径:never() 与 thenThrow() 的黄金组合
健壮的测试必须覆盖异常场景,而 verify() 在其中扮演“负向验证”的角色。典型模式是: 当被测代码因依赖抛异常而进入错误处理分支时,验证“不该发生的动作确实没发生” 。
例如用户注销功能:调用 userService.logout(userId) 时,先调用 tokenService.invalidateToken(userId) ,再调用 notificationService.sendLogoutNotice(userId) 。但如果 tokenService 抛出 TokenInvalidationException ,则不应发送通知。
测试代码应这样写:
// 预设异常
when(tokenService.invalidateToken(eq(userId))).thenThrow(new TokenInvalidationException("invalid token"));
// 执行被测方法
userService.logout(userId);
// 验证:token 失效失败,所以通知绝对不能发
verify(tokenService).invalidateToken(eq(userId));
verify(notificationService, never()).sendLogoutNotice(eq(userId));
这里 never() 是关键。如果只写 verify(notificationService).sendLogoutNotice(...) ,测试会因“未调用”而失败;如果什么都不 verify,就失去了对错误路径的控制。 never() 强制你思考:“在这个异常条件下,哪些协作必须被阻止?”
另一个高阶技巧是结合 willDoNothing() 和 never() 验证“空实现”场景。比如一个新接入的风控服务,当前阶段只做日志记录,不实际拦截。你可以:
// 预设空行为
doNothing().when(riskService).check(eq(userId));
// 执行
userService.register(newUser);
// 验证:风控服务被调用,但不期望它改变任何状态(如拒绝注册)
verify(riskService).check(eq(userId));
// 注意:这里不用 never(),因为调用是期望的,只是行为为空
4.2 验证私有方法调用:PowerMock 时代已过,用 Spy + verify() 更优雅
Java 单元测试中,“如何测试私有方法”曾是长期争议点。PowerMock 通过字节码增强能直接 mock 私有方法,但代价是破坏了测试的纯净性(需要 JVM agent、与 JUnit5 兼容性差)。现代 Mockito(3.4.0+)提供了更轻量的方案: Spy + verify() 。
原理是:Spy 是对真实对象的包装,它会执行真实方法,但同时记录所有调用。对于私有方法,我们可以通过反射将其设为可访问,然后在 Spy 中调用:
// 创建 spy
UserService spyUserService = spy(new UserServiceImpl());
// 通过反射获取私有方法
Method privateMethod = UserServiceImpl.class.getDeclaredMethod("validatePassword", String.class);
privateMethod.setAccessible(true);
// 执行私有方法(通过 spy,以便记录调用)
privateMethod.invoke(spyUserService, "weak123");
// 验证私有方法被调用
verifyPrivate(spyUserService).invoke("validatePassword", "weak123");
注意: verifyPrivate() 是 Mockito 的扩展 API,需引入 mockito-inline 依赖。它本质上是 Spy 机制的封装,比 PowerMock 更安全、更易调试。
但更重要的经验是: 90% 的私有方法测试需求,其实源于设计缺陷 。如果一个私有方法逻辑复杂到需要单独测试,它很可能本该是 public 的工具类方法。我的建议是:优先重构,将核心算法提取到独立 service;实在无法重构时,再用 Spy 方案。
4.3 微服务场景下的 verify():Mock 外部 HTTP Client 的完整链路
在 Spring Boot 微服务中,调用其他服务通常通过 RestTemplate 或 WebClient 。验证这些调用,是 verify() 的高价值场景。以 RestTemplate 为例:
// 创建 mock restTemplate
RestTemplate mockRestTemplate = mock(RestTemplate.class);
// 注入到被测 service
OrderService orderService = new OrderServiceImpl(mockRestTemplate);
// 预设响应
when(mockRestTemplate.postForObject(
eq("https://inventory-service/api/inventory/check"),
argThat(req -> req.getProductId().equals("P123")),
eq(String.class)))
.thenReturn("{\"available\":true}");
// 执行
orderService.createOrder(order);
// 验证:不仅调用 URL 正确,参数对象也符合预期
verify(mockRestTemplate).postForObject(
eq("https://inventory-service/api/inventory/check"),
argThat(req -> req.getProductId().equals("P123") && req.getQuantity() == 1),
eq(String.class)
);
这里的关键是: verify() 的参数匹配必须与 when() 的预设完全对称 。如果 when() 用 any() , verify() 也得用 any() ;如果 when() 用 eq("url") , verify() 也必须用 eq("url") 。否则 verify 会找不到记录。
对于 WebClient(响应式编程),由于它是链式调用,不能直接 mock,需用 MockWebServer (Square 提供):
// 启动 mock server
MockWebServer server = new MockWebServer();
server.start();
String baseUrl = "http://" + server.getHostName() + ":" + server.getPort();
// 预设响应
server.enqueue(new MockResponse()
.setResponseCode(200)
.setBody("{\"status\":\"success\"}"));
// 创建 WebClient 指向 mock server
WebClient webClient = WebClient.builder()
.baseUrl(baseUrl)
.build();
// 执行
Mono<String> result = webClient.get()
.uri("/api/payment")
.retrieve()
.bodyToMono(String.class);
// 验证:检查 mock server 是否收到请求
RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS);
assertThat(request.getPath()).isEqualTo("/api/payment");
assertThat(request.getMethod()).isEqualTo("GET");
注意: MockWebServer 的验证是“服务端视角”,而 verify() 是“客户端视角”。两者互补,共同构成完整的 HTTP 调用验证。
4.4 verify() 的性能陷阱:避免在循环中滥用,用批量验证替代
一个隐蔽但致命的性能问题:在 for 循环中反复调用 verify() 。例如处理订单列表:
// ❌ 错误:N 次 verify,每次都要遍历整个调用记录队列
for (Order order : orders) {
verify(orderRepository).save(eq(order));
}
// ✅ 正确:一次 verify 捕获所有参数,再用断言验证
ArgumentCaptor<Order> orderCaptor = ArgumentCaptor.forClass(Order.class);
verify(orderRepository, times(orders.size())).save(orderCaptor.capture());
List<Order> savedOrders = orderCaptor.getAllValues();
assertThat(savedOrders).containsAll(orders);
原因在于:每次 verify() 都会从头扫描 InvocationContainer ,时间复杂度 O(N*M),其中 N 是调用次数,M 是 verify 次数。当 orders.size() 为 1000 时,扫描次数高达百万级。而 ArgumentCaptor 只扫描一次,时间复杂度 O(N)。
另一个陷阱是 verifyNoInteractions(mock) 的误用。它检查 mock 是否被任何方法调用过,但内部实现是遍历所有记录并过滤。如果 mock 被大量无关方法调用(如 toString() 、 hashCode() ),这个验证会很慢。更好的做法是: 只对明确关心的 mock 调用 verify,忽略无关交互 。
5. 常见问题与实战排查:那些让资深工程师也挠头的 verify() 异常
5.1 WantedButNotInvoked:不是“没调用”,而是“没找到匹配记录”
这是 verify() 最常见的异常,错误信息类似: Wanted but not invoked: mock.process("order-123"); Actually, there were zero interactions with this mock.
90% 的情况并非代码没执行,而是 参数匹配失败 。排查步骤必须按此顺序:
- 确认被测代码真的执行了 :在被测方法入口加
System.out.println("entering process..."),或用 IDE 断点确认; - 检查 verify 的参数是否与实际调用完全一致 :
- 字符串是否有多余空格?
"order-123 "vs"order-123"; - 数值是否类型不匹配?
Long.valueOf(123)vsInteger.valueOf(123); - 对象是否用了
eq()但equals()方法有 bug?
- 字符串是否有多余空格?
- 确认 mock 对象是同一个实例 :常见错误是
@Mock和mock()混用,或在测试中 new 了新 mock; - 检查是否在 verify 前重置了 mock :
reset(mock)会清空所有调用记录。
我处理过一个典型案例:测试中 verify(mock).send(eq(message)) 失败,但 message.toString() 输出完全一致。最后发现 message 是 Lombok 生成的, @Data 注解未包含 @EqualsAndHashCode ,导致 eq() 比较失败。解决方案是 refEq(message) 或为 message 添加 @EqualsAndHashCode 。
5.2 TooManyActualInvocations:不是“调多了”,而是“验证模式太窄”
异常信息如: Too many actual invocations: mock.process("order-123"); expected: 1, actual: 2
这通常意味着:
- 业务代码中存在重复调用(如循环内未 break);
verify()的参数匹配过于宽泛,捕获了不该捕获的调用。
例如, verify(mock).process(anyString()) 会匹配 process("order-123") 和 process("log-debug") ,如果后者是日志方法,就造成误统计。解决方案是收紧匹配: verify(mock).process(startsWith("order-")) 或 verify(mock).process(argThat(s -> s.startsWith("order-"))) 。
另一个原因是 @Spy 的误用。Spy 会执行真实方法,如果真实方法内部又调用了自己(递归),就会产生额外记录。此时应改用 @Mock ,或在 when() 中预设返回值阻止递归。
5.3 与 @Transactional 的冲突:为什么 verify() 在事务方法里总是失败?
Spring 的 @Transactional 会让方法在代理对象中执行,而 Mockito 的 mock 是独立对象。当被测类是 @Service 且方法加了 @Transactional ,测试中直接调用 service.method() 实际走的是 Spring 代理,mock 的调用记录不会被记录到 Mockito 的 InvocationContainer 中。
解决方案有二:
- 测试类也加
@Transactional:让测试运行在同一个事务上下文,代理链路保持一致; - 用
@MockBean替代@Mock:@MockBean会替换 Spring 容器中的 bean,确保代理能捕获到调用。
@SpringBootTest
class OrderServiceTest {
@MockBean // 关键:注入容器,而非局部 mock
private InventoryService inventoryService;
@Autowired
private OrderService orderService;
@Test
void testCreateOrder() {
when(inventoryService.checkStock(any())).thenReturn(true);
orderService.createOrder(new Order());
verify(inventoryService).checkStock(any()); // ✅ 成功
}
}
5.4 verify() 与异步线程的竞态:timeout() 不是万能解药
verify(mock, timeout(1000)).doAsync() 失败,不一定是因为异步没完成,而可能是:
timeout()的轮询间隔(默认 100ms)与异步任务的实际耗时不匹配;- 异步任务抛出了未捕获异常,导致线程静默退出;
verify()执行时,异步线程还在初始化(如线程池未 warm up)。
诊断技巧:
- 在异步任务开始和结束处加日志,确认是否执行;
- 将
timeout(1000)改为timeout(5000),观察是否稳定; - 用
CountDownLatch在测试中同步等待:
CountDownLatch latch = new CountDownLatch(1);
when(asyncService.doAsync()).thenAnswer(inv -> {
latch.countDown();
return null;
});
// 执行
latch.await(5, TimeUnit.SECONDS); // 确保异步启动
verify(asyncService).doAsync(); // 此时 verify 更可靠
5.5 verify() 的“幽灵调用”:静态方法、构造器、final 方法为何无法 verify?
Mockito 无法 mock 静态方法、final 方法、构造器,因为它们在字节码层面无法被代理。如果你看到 verify(mock).staticMethod() 编译失败,或 verify(mock).finalMethod() 运行时报 Cannot mock/spy because ,说明你试图验证一个 Mockito 无权干涉的行为。
解决方案:
- 静态方法 :用
mockito-inline+mockStatic()(Mockito 3.4.0+); - final 方法 :用
mockito-inline,或重构为非 final; - 构造器 :用
@InjectMocks+@Mock模拟依赖,避免在构造器中做重操作。
例如验证 LocalDateTime.now() :
try (MockedStatic<LocalDateTime> mocked = mockStatic(LocalDateTime.class)) {
mocked.when(LocalDateTime::now).thenReturn(LocalDateTime.of(2023, 1, 1, 0, 0));
// 执行
verifyStatic(); // 验证静态方法被调用
LocalDateTime.now();
}
6. 高阶模式与架构启示:verify() 如何塑造可测试的系统设计
6.1 “验证驱动开发”(VDD):用 verify() 倒逼接口契约清晰化
传统 TDD(测试驱动开发)关注“输入-输出”,而 VDD(Verification-Driven Development)关注“协作-契约”。它的实践流程是:
- 在编写被测代码前,先写
verify()语句,明确“这个类需要和谁协作?以什么方式协作?”; - 根据
verify()的参数需求,定义依赖接口的方法签名和参数类型; - 实现被测代码,使其满足
verify()的调用约定。
例如开发一个报表导出服务:
- 第一步写
verify(reportGenerator).generate(eq("sales-report"), argThat(params -> params.getYear() == 2023)); - 第二步定义
ReportGenerator.generate(String reportType, ReportParams params); - 第三步实现
SalesReportService.export(),确保它调用reportGenerator.generate()且参数符合预期。
这种方法强制你在编码前思考: 这个类的职责边界在哪里?它依赖的抽象是否足够稳定?参数是否可序列化、可比较? 我带领的一个团队采用 VDD 后,接口变更率下降 40%,因为 `verify
更多推荐


所有评论(0)