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 方法,并在每个方法入口处插入了一段“记录逻辑”:

  1. 捕获当前方法名、参数值(序列化副本)、调用时间戳;
  2. 将这条记录存入一个线程局部变量(ThreadLocal)维护的 InvocationContainer 中;
  3. 如果该方法被 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 秒后仍未发现,则抛出异常

这意味着它有两个关键特性:

  1. 非阻塞式轮询 :Mockito 默认每 100ms 检查一次队列,不是挂起线程傻等;
  2. 超时是硬性截止 :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 就是最佳选择。

使用流程分三步:

  1. 声明 captor: ArgumentCaptor<Order> orderCaptor = ArgumentCaptor.forClass(Order.class);
  2. 在 verify 中捕获: verify(orderService).process(orderCaptor.capture());
  3. 对捕获的参数进行断言:
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% 的情况并非代码没执行,而是 参数匹配失败 。排查步骤必须按此顺序:

  1. 确认被测代码真的执行了 :在被测方法入口加 System.out.println("entering process...") ,或用 IDE 断点确认;
  2. 检查 verify 的参数是否与实际调用完全一致
    • 字符串是否有多余空格? "order-123 " vs "order-123"
    • 数值是否类型不匹配? Long.valueOf(123) vs Integer.valueOf(123)
    • 对象是否用了 eq() equals() 方法有 bug?
  3. 确认 mock 对象是同一个实例 :常见错误是 @Mock mock() 混用,或在测试中 new 了新 mock;
  4. 检查是否在 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 中。

解决方案有二:

  1. 测试类也加 @Transactional :让测试运行在同一个事务上下文,代理链路保持一致;
  2. @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)关注“协作-契约”。它的实践流程是:

  1. 在编写被测代码前,先写 verify() 语句,明确“这个类需要和谁协作?以什么方式协作?”;
  2. 根据 verify() 的参数需求,定义依赖接口的方法签名和参数类型;
  3. 实现被测代码,使其满足 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

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐