Mockito从入门到进阶:打桩、验证与单测实战

发布时间:2026/10/13 15:51:57
Mockito从入门到进阶:打桩、验证与单测实战 Mockito 这个框架在 Java 后端开发的日常里几乎到了“只要写单元测试就不可能绕开”的地步。很多人刚开始接触它时唯一的感受就是“能少写一堆乱七八糟的依赖对象”但真正用熟了之后会发现它的价值远不止“造假的替身演员”。虚拟数据、交互验证、异常模拟、行为编排这些东西真正掌握之后单测写起来不仅快而且稳能够在没有数据库、没有外部接口、没有中间件的情况下把核心业务逻辑牢牢锁死。这篇文章想梳理的就是 Mockito 从入门到进阶的主线。假设你已经知道它是 Java 里做 mock 的工具但对内部机制和高级用法还停留在“能用但心里没底”的阶段那这篇文章正适合你。我尽量讲清楚两个核心主题一是虚拟数据怎么设计、怎么打桩才不会踩坑二是验证机制怎么用“验证”不是简单调一下 verify 就完事它背后有交互语义、有调用次数、有执行顺序把这套东西吃透很多隐藏 issue 才有机会在开发阶段浮出水面。1. 为什么需要 Mockito单测里最稀缺的是“可控性”单元测试的目标是孤立的验证一段代码逻辑在给定的输入下是否产生期望的输出或行为。但现实世界的 Java 工程里几乎没有哪个服务类可以脱离依赖独立存活。你写一个订单服务它要调用户服务查会员等级要调库存服务锁定库存要调支付通道发起扣款。在测试环境里这些依赖要么没部署要么数据没法预置要么响应速度不稳定。真正让人崩溃的是“不可控”——你没法保证调用某接口时它恰好返回你想要的超时异常、空指针、金额不足这几种情况所以单元测试的边界往往就打不出去。Mockito 的定位就是帮你在内存中创建一个“假对象”。这听起来简单但意义很大。它让你把被测试对象的依赖关系显式声明出来哪个依赖给什么返回值哪个依赖抛什么异常哪个依赖根本不需要真正执行。被测代码在跑的时候只会和这些事先安排好的假对象交互整个过程完全由你控制。正因为这种可控性单测才能在毫秒级跑完且不依赖任何外部环境任何人在任何机器上都能重复验证同一套逻辑。我一直把 mock 理解为“测试中的替身演员调度系统”。真实演员是接口的实现类你替它安排一个替身替身按剧本给出反应。Mockito 的核心价值不是“造假”而是让“假”变得精确、可预期、可观察。精确指的是返回值由你定义可预期指的是每次调用按你的规则响应可观察指的是 mock 对象记录了自己被调用的历史测试结束时你能检查它是否符合预期。同时要理解的是mock 和桩stub之间是有细微区别的。很多初学者混用这两个词这在讨论技术时会引发误解。Stub 重点是“状态”——给某个方法调用设置固定的返回用来支撑被测代码往下走。Mock 重点是“行为交互”——除了设置返回值还关注方法是否被调用过、调用了几次、调用顺序等。Mockito 其实两者都支持所以它能同时承担“虚拟数据供给”和“交互行为验证”这两件不同的事。后面第二、第三大节会分别展开这两条线的具体玩法。2. 虚拟数据的正确姿势打桩前先想清楚“谁来产生值”2.1 打桩的两种语义when...thenReturn 和 doReturn...whenMockito 里最经典的一段代码长这样when(mockUserService.getMemberLevel(u_10086)).thenReturn(MemberLevel.VIP);这段代码的含义是当 mock 出来的用户服务收到参数“u_10086”时返回一个 VIP 等级。执行到这一行时Mockito 并没有真正调用getMemberLevel方法它只是在记录一个“行为规则”。这个设计是 Mockito 的底层核心方法调用在这里不是真的“执行”而是在 stubbing 阶段被拦截写入到 mock 内部的一个规则集合里。等被测代码真正调用这个方法时Mockito 会去查这个规则集合按参数匹配规则返回对应的结果。实际操作中有一个细节需要注意**如果打桩的方法返回 void或者你想在打桩时强制执行某个副作用比如抛出异常就不能用 when 语法要用 do...when 语法。**最典型的例子是模拟 void 方法抛异常doThrow(new IllegalStateException(库存不足)) .when(mockStockService) .lockStock(anyString(), anyInt());为什么这里必须用 doThrow 而不是when(mockStockService.lockStock(...))原因是当你写when(mockStockService.lockStock(...))时lockStock会被真的执行一次。对于 void 方法它在执行时什么都不返回自然没法放到when(...)的参数位置去“声明规则”。Mockito 的 when 本质上是“先捕获一次调用结果再定义规则”void 方法没有可供捕获的返回值所以只能用从 do 开头的那组 API。同理如果你打桩的对象方法在调用时会抛异常用 when 语法也会在打桩那行提前把异常抛出来导致打桩失败这时 do 开头的方法就是更安全的选择。另一个值得记住的经验是“能不打桩就不要打桩”。Mockito 对未打桩的方法有一套默认策略对象类型返回空对象比如 List 返回空列表Map 返回空 Map布尔返回 falseint 返回 0。虽然可以通过RETURNS_DEEP_STUBS、RETURNS_SMART_NULLS修改默认策略但大多数场景下依赖默认返回能把测试类精简不少。如果你的业务代码会在依赖返回 null 时产生 NPE那说明代码本身的健壮性存疑单元测试正好帮你暴露出来。刻意把所有方法都打桩除了让测试代码膨胀、可读性下降之外没有额外的好处。2.2 参数匹配器从精确匹配到“任何值”与“组合条件”写打桩时最自然的想法是对具体参数精确匹配when(obj.method(exact)).thenReturn(value)。但在真实业务里被测代码传过来的参数往往不是预先能确定的常量或一个测试里同一个方法会被多次调用、需要返回不同结果。这时候就要用参数匹配器ArgumentMatcher。Mockito 内置了一批常用匹配器any()、anyString()、anyInt()、anyList()、eq()、isNull()、isNotNull()、contains()、startsWith()等。比如when(stockService.queryStock(anyString(), anyInt())).thenReturn(50); when(stockService.queryStock(eq(sku_9527), anyInt())).thenReturn(200);这里 eq 很重要因为当你用anyString()匹配第一个参数时第二个参数千万不能直接写字面量100而必须写成eq(100)。Mockito 的规则是只要参数列表里出现一个匹配器那么所有参数都必须使用匹配器直接写字面量会被当成“equals(字面量)”匹配混用直接抛异常。这个限制在初学阶段坑了我好几次排查半天才发现是混用导致的。有一点要特别小心匹配器和“返回值顺序”结合时策略到底是什么。如果你对同一个方法打了两条桩参数分别为a和bMockito 会走最近匹配、最后打桩优先的策略。即如果第一条规则是a返回 1第二条规则是anyString()返回 2当实际参数是a时匹配到第一条规则返回 1当实际参数是c时匹配到第二条规则返回 2。如果你把两条规则顺序反过来所有字符串参数都会命中anyString()第二个更具体的a规则永远不会生效。所以写打桩时规则顺序也是一种隐性决策通用的放前面、具体的放后面或者干脆只保留必要规则尽量减少重叠匹配带来的歧义。对于复杂参数对象除了用eq(exactObject)更优雅的做法是用 ArgumentMatcher 接口自定义匹配逻辑。例如要验证传入的订单对象金额大于 1000 才返回某个结果可以写when(orderRepository.save(argThat(order - order.getAmount() 1000))) .thenReturn(savedOrder);argThat接收一个 Predicate 风格的参数在运行时对传入参数进行判定。这种写法特别适合那些字段多、batch 处理、按业务规则分类的场景。不过自定义匹配器会让报错信息变得不直观如果匹配失败Mockito 只能告诉你“这个参数不匹配”但到底哪里不匹配就无法提示了。所以能用内置匹配器就用内置自定义匹配器尽量用在高层次语义判断上而不是低级字段比对。2.3 连续调用与动态返回值thenReturn 的进阶用法有时一个 mock 方法在同一个测试里被调用三次你希望第一次返回 null第二次返回异常第三次返回一个对象。Mockito 支持链式打桩when(userService.getUser(u_1)) .thenReturn(null) .thenThrow(new RuntimeException(被锁定)) .thenReturn(user);也可以写成多参版本thenReturn(null, user)表示第一次返回 null、第二次以及后续返回 user。它的内存逻辑是维护了一个 Iterator每次调用按顺序消费下一个规则。这种链式写法的可读性比连续写多条 when 好很多尤其适合“模拟依赖不稳定”的场景。但注意这种顺序消费规则只对“没有更精确匹配规则”的调用生效。如果你既有链式规则又有精确参数规则匹配优先级会按参数精确度而不是链式顺序决定所以设计时要避免规则冲突。更自由的动态返回值通过thenAnswer实现。它允许基于入参实时计算结果when(calculatorService.calculate(anyDouble(), anyDouble())) .thenAnswer(invocation - { double a invocation.getArgument(0); double b invocation.getArgument(1); return a * b 100; });thenAnswer的入参是 InvocationOnMock通过它你能拿到调用对象、方法、参数列表甚至能调用真实方法。这对模拟“依赖内部状态”的场景很有用。比如一个 getUser 方法要求传入 id 为 null 时抛异常id 存在时返回对应的用户对象而用户对象需要从某个内存 Map 里查这时 thenAnswer 比写一堆 thenReturn 链要灵活得多。实际项目中我倾向把复杂动态逻辑封装成独立的 Answer 类而不是在测试里写一大段 lambda因为一旦 lambda 太复杂测试的意图就被冲淡了。3. 验证机制的核心从“返回值正确”到“交互行为正确”3.1 verify 的本质记录、匹配与断言单测里有一类 bug 特别隐蔽方法返回了正确结果但内部交互不符合预期比如本该调用缓存服务却直接查了数据库本该调用三次批量接口却只调了一次。Mockito 的 verify 就是为这类问题设计的。每个 mock 对象从创建开始就在内部记录了一份“调用日志”哪个方法、什么参数、调用了几次、顺序如何。verify 做的事情是回放这些日志按照你给定的条件进行匹配匹配失败则抛出 VerificationException。最简单的验证是检查某个方法是否被调用verify(mockUserService).getUser(u_1);等价于验证getUser(u_1)被调用且仅调用了一次。想验证调用次数时verify(mockUserService, times(3)).getUser(u_1); verify(mockUserService, atLeastOnce()).getUser(anyString()); verify(mockUserService, atMost(2)).getUser(anyString()); verify(mockUserService, never()).deleteUser(anyString());times(n)里的 n 必须精确匹配多一次少一次都报错。atLeastOnce和atMost则给出边界条件。这里有一个容易忽略的点never()等价于times(0)它用于验证某些“不应该发生”的调用比如灰度开关关闭时不应该上报监控数据这时verify(mockMonitor, never()).report(...)是有效且低成本的安全网。验证时同样可以使用参数匹配器。规则和打桩时一致只要出现一个匹配器所有参数都必须用匹配器。一个比较常见的错误是验证时用anyString()但业务代码传了 nullanyString()不匹配 null 值验证就会失败。此时要根据业务需要选择nullable(String.class)或者isNull()。这类“null 值边界”在验证阶段极容易遗漏因为代码逻辑在测试数据设置正确时能跑通但线上真实场景接受到了 null 就崩。3.2 验证异常与状态改变的交叉场景verify 并不只验证“有没有调用”它对测试的决策价值在于**它能确认某个分支是否被执行。**比如被测代码处理逻辑是这样的调用外部接口后如果返回码是 200就更新本地状态并发送一条事件如果返回码是 500则记录异常日志并抛业务异常。你分别写两个测试第一个测试打桩外部接口返回码为 200执行被测逻辑后verify(mockEventPublisher).publish(any())。第二个测试打桩外部接口返回码为 500执行被测逻辑后verify(mockEventPublisher, never()).publish(any())同时assertThrows(BusinessException.class, () - service.doSomething())。这样一套组合下来分支覆盖就非常清楚。测试框架能保证“只要走某条分支就一定有对应的行为发生”而 verify 就是这个行为是否发生的证据。实际写测试的时候我习惯把“打桩目标”和“验证目标”分开识别打桩目标通常是外部依赖验证目标通常是被测代码自身暴露出的协作对象。如果一个对象只用做打桩、从来不需要验证那它很可能只是被测对象的内部工具不需要暴露在测试视野里。另一个容易踩坑的细节是 verify 方法的重载与超时验证。Mockito 支持timeout()例如verify(mock, timeout(1000)).asyncCall()。但 timeout 和after()更适用于异步场景普通同步单测中不推荐使用因为 timeout 会导致测试时长不可控。专跑异步测试时稳妥的方案是 mock 的回调里用 CountDownLatch 或其他同步工具把异步转成同步来做断言而不是把 verify 放在 time interval 里轮询。3.3 InOrder 与交互顺序当“先做什么”成为业务规则有些业务逻辑对调用顺序有硬性要求。例如用户注册时必须先检查账号是否存在、再发送验证码、最后写入数据库顺序错了就是安全漏洞。Mockito 提供一个轻量级顺序验证工具 InOrderInOrder inOrder inOrder(mockUserRepo, mockSmsService); inOrder.verify(mockUserRepo).existsByPhone(13800000000); inOrder.verify(mockSmsService).sendCode(13800000000, anyString()); inOrder.verify(mockUserRepo).save(any(User.class));这段验证的价值在于如果被测代码在执行过程中调整了顺序比如先存库再发验证码测试立刻失败提醒你业务语义被破坏了。多 mock 对象之间的顺序也可以跨对象验证InOrder 的构造参数传入所有要验证的 mock 即可。注意 InOrder 和普通 verify 是独立的验证通道同一个 mock 既可以通过普通 verify 验证次数也可以通过 inOrder 验证顺序二者不冲突。需要提醒的是顺序验证要克制使用。不是所有方法调用都有顺序语义有些调用本来就是无序的例如多个统计上报。给无序调用强行加上顺序验证会让测试变得脆弱——没错今天的代码也许是有序的但明天某个无关重构打乱了调用顺序本来不影响正确性测试却红了这会白白消耗团队时间。我个人的判断标准是顺序是不是业务规则的一部分如果是比如“先鉴权后操作”就验证如果不是比如“记录日志和写审计表”就让它们保持在无序状态。3.4 验证失败时如何快速定位自定义描述与错误消息Mockito 的验证失败信息通常长这样Argument(s) are different! Wanted: mockService.getUser(u_1); Actual invocations have different arguments: mockService.getUser(u_2);大多数情况下这个信息已经够用。但复杂场景下比如同一个方法被调了 20 次或者参数是一个字段很多的 DTO光看差异信息很难定位问题。这时可以在 verify 时使用描述性错误提升可读性verify(mockService, description(缓存加载时应该只调用一次远程接口)).times(1).load(anyString());当验证失败时Mockito 会先打出这句描述再打出具体差异信息。这个description()是成本极低的调试辅助特别适合跑在 CI 里别人看不到你本地调试信息的场景。写测试代码的人要想象自己是未来接手这个测试的同事看到一行 verify 失败时能不能根据失败信息快速判断是业务逻辑变了还是 mock 配置错了。良好的描述信息能显著降低团队在单测维护上的痛苦。4. 高级实践技巧Spy、静态方法与 BigDecimal 这类特殊边缘4.1 Spy保留真实行为只替换局部依赖Mock 和 Spy 最直观的区别在于Mock 对象的所有方法默认不执行真实逻辑而 Spy 对象默认执行真实逻辑只有显式打桩的方法才会被替换。说得更直白一点Spy 是“基于真实对象的部分 mock”。创建 Spy 的推荐方式是对真实对象实例进行包装UserService realService new UserService(); UserService spyService spy(realService); doReturn(vipUser).when(spyService).getUser(u_vip);大多数场景里Spy 的价值在于测试那些“内部方法”和“外部方法”混杂的旧代码。例如 Service 里有一个私有工具方法计算折扣一个公有方法调用它。原则上应该把折扣计算抽出来做纯函数测试但老代码不好拆时可以用 Spy 只替换公有的部分保留折扣计算的真实逻辑。这种“缝合”式测试在遗留系统维护中很常见也是 Mockito 比一些“纯 mock 世界观”框架更务实的原因。但 Spy 有一些明显的坑。第一个坑是对象初始化依赖的外部资源如果你spy(new UserService())但 UserService 的构造函数里连接了数据库那 Spy 创建时还是会真连接数据库测不起来。这种就得用无参构造或者反射设置字段或者把对象设计成可依赖注入的结构。第二个坑是when(spyObj.method())语法它对 spy 对象使用 when 时方法会被真实执行一次所以如果方法有副作用必须用doReturn().when()避免真实调用。这一点我在写代码时几乎已经形成了肌肉记忆凡是 spy打桩一律 do 开头绝不用 when 开头。4.2 静态方法与构造方法的 mockMockedStatic 与 MockedConstruction从某个版本开始Mockito 支持 mock 静态方法这解决了不少“工具类直接调用”的问题。基本写法try (MockedStaticIdGenerator mockedStatic mockStatic(IdGenerator.class)) { mockedStatic.when(IdGenerator::nextId).thenReturn(mock_id_001); // 业务逻辑 }注意MockedStatic必须声明在 try-with-resources 块里作用域限定在块内块结束自动释放 mock避免影响其他测试。这一点常被忽略如果你在测试方法级别忘记关闭静态方法 mock 会漂移到别的测试用例中造成诡异的测试相互污染。在写测试基类时建议将MockedStatic的生命周期严格对齐到测试方法的边界而不是类级别。构造方法的 mock 用MockedConstruction适用于工厂模式、依赖注入我没有办法处理的场景。比如被测代码内部new ExternalCalculator()这个 ExternalCalculator 来自第三方库无法直接替换则try (MockedConstructionExternalCalculator mockedConstruction mockConstruction(ExternalCalculator.class)) { ExternalCalculator calc new ExternalCalculator(); when(calc.add(1, 2)).thenReturn(99); // 业务逻辑里 new 出来的是同一个构造 mock }这个特性用起来舒服但负担也明显会让被测代码里的“新建对象”行为变得不再直观测试阅读者需要额外寻找构造 mock 的位置。建议只在真的无法重构依赖注入的老代码中使用新代码优先推荐构造器注入直接通过 Mock InjectMocks 来装配会清爽得多。静态与构造 mock 让 Mockito 在“遗留代码”和“第三方库依赖”场景下活得更滋润但我仍建议把它当作“最后的手段”。理由很简单过度使用会让单测与实现细节耦合更紧重构时测试会先崩反而不利于代码演进。把可测性设计在代码结构里才是长期最优解。4.3 Object 类型参数、BigDecimal 和泛型的匹配细节Java 的泛型在 Mockito 里有一套自己的语法。比如 mock 一个ListStringListString mockList mock(List.class);通常没问题但如果要打桩when(mockList.get(0)).thenReturn(hello)编译器可能提示类型不匹配因为 mock(List.class) 丢掉泛型信息。此时有两个选择直接使用Mock ListString mockList;让注解处理器帮你处理泛型或者使用ArgumentMatchers.anyList()时配合anyList()返回值是ListT让类型推断生效。在 Java 7 类型推断不够强的年代这很痛苦现代 JDK 下配合注解基本无感。BigDecimal 是另一个高频特殊对象。Mockito 的默认RETURNS_DEFAULTS对 BigDecimal 返回null不是 0所以如果你打桩一个方法返回 BigDecimal必须显式给值不能指望像 int 那样拿到 0。另一个常见问题是equals语义BigDecimal 的equals比较值和精度new BigDecimal(1.0)与new BigDecimal(1.00)不相等所以参数匹配eq(new BigDecimal(1.0))时如果业务代码传的是 1.00 就会匹配失败。实际处理时优先用argThat或compareTo方式作匹配按数值比较而不是严格 equals。泛型和特殊数值类型听起来细节但往往就是这些细节让同一套测试在开发环境绿、在 CI 上红。排查久了你会明白Mockito 匹配的是运行时参数的实际 equals 或匹配器逻辑不是编译期的类型兼容。5. 与 JUnit 5、Spring Boot 单测的整合方式5.1 JUnit 5 集成junit-jupiter 与 MockitoExtension现代 Java 工程几乎都在 JUnit 5 体系下Mockito 官方提供了mockito-junit-jupiter模块让 Mockito 和 JUnit 5 无缝协作。引入依赖后可以用ExtendWith(MockitoExtension.class)激活 Mockito 注解支持。ExtendWith(MockitoExtension.class) class OrderServiceTest { Mock private StockService stockService; Mock private UserService userService; InjectMocks private OrderService orderService; Test void testCreateOrder() { when(stockService.lockStock(anyString(), anyInt())).thenReturn(true); when(userService.getUser(anyString())).thenReturn(vipUser()); Order result orderService.createOrder(sku_1001, 2); assertNotNull(result); verify(stockService).lockStock(sku_1001, 2); verify(userService).getUser(anyString()); } }InjectMocks会按类型和名称把 mock 字段注入到被测实例里支持构造器注入、Setter 注入和字段注入。写测试时推荐优先用构造器注入因为构造器会把依赖关系显式化Mockito 注入时也更稳定。如果被测类没有构造器而是大量字段注入InjectMocks也能工作但测试一旦跑起来报注入失败排查难度会大不少。一个极其重要的细节默认情况下MockitoExtension 引入的是严格打桩检测Strict Stubs这意味着如果测试里定义了某个打桩但从未被实际调用测试会直接失败。这是把双刃剑。好处是它能提醒你打桩冗余帮测试瘦身坏处是有些“防御性打桩”会在分支切换时变得无用反而让测试变红。如果你确定某个打桩就是备而不用可以在打桩后调用lenient()lenient().when(stockService.queryStock(sku_1)).thenReturn(100);严格模式不是灾难它会强迫你反思每一根桩是否真的必要长期看对测试维护是好事。刚切换到 JUnit 5 MockitoExtension 的团队往往会先被严格模式误杀一波调整代码后就会习惯。5.2 Spring Boot 测试中如何切掉外部依赖Spring Boot 项目里常见的是SpringBootTest加载整个上下文。但单测一旦拉起了 Spring 容器就很容易碰到数据库、Redis、消息队列等真实资源。想让单测跑得快且对环境零依赖需要谨慎对待 Spring Test 上下文。并非所有测试都推荐SpringBootTest纯单元测试可以直接用纯 Mockito只测单个类的逻辑不加载任何 Spring 上下文。骨架如下被测类直接 new依赖用 Mock 装配根本不跑 Spring 容器。只有在测 Controller 层、过滤器、切面等需要考虑 Spring 容器组件的场景才建议WebMvcTest配合MockBean或MockitoBean来切掉外部依赖。MockitoBean是 Spring Boot 测试中声明 mock Bean 的注解它会把对应的 mock 替换掉容器中的真实 Bean让被测 Controller 调用时命中 mock。用起来非常简单WebMvcTest(OrderController.class) class OrderControllerTest { Autowired private MockMvc mockMvc; MockitoBean private OrderService orderService; Test void testCreate() throws Exception { when(orderService.createOrder(any())).thenReturn(orderResult()); mockMvc.perform(post(/orders) .contentType(MediaType.APPLICATION_JSON) .content({\skuId\:\sku_1001\,\num\:2})) .andExpect(status().isOk()) .andExpect(jsonPath($.orderId).value(order_001)); } }值得一提MockitoBean是较新版本中重新命名的方案早期叫MockBean新版本 Spring 官方推荐前者因为它语义更明确。如果你的项目版本不支持退回MockBean也可以。这两种注解本质上都是 Mockito 创建的 mock 替换了容器中的 Bean。Spring 测试和纯 Mockito 测试在我心目中的边界很清晰纯 Mockito 解决的是“类级逻辑”Spring 测试解决的是“语境装配、序列化、HTTP 层、过滤器链”。一个经验法则是尽量把业务逻辑放到 Service 层Service 层的每个方法尽量能被纯 Mockito 测试Controller 层只做参数接收和响应转换用 WebMvcTest 做轻量级验证。这样整个测试金字塔结构才合理不会因为加载 Spring 上下文导致一套测试跑几分钟。5.3 测试隔离为什么不要共享 Mockito 的 Mock 状态JUnit 默认每个测试方法创建新的测试类实例所以 Mock 字段默认也是每个方法一份互相不干扰。但如果你在类级别定义了static Mock或者使用BeforeAll去初始化 mock那 mock 的调用记录就会跨测试共享。跨测试共享的 mock 是测试相互污染的经典来源前一个测试打桩的规则还在后一个测试的 verify 会接收到前一个测试的调用记录导致断言失败。实践中我见过不少同事为了“提升速度”在类里静态初始化 Mock 对象结果反而因为状态污染浪费更多时间排查。Mockito 的 mock 对象创建成本极低完全没必要做静态共享。如果要复用一些通用的打桩规则可以封装在 private 方法里返回 mockprivate UserService mockVipUserService() { UserService mock mock(UserService.class); when(mock.getLevel(anyString())).thenReturn(VIP); return mock; }然后每个测试方法独立创建使用既保留复用性又避免状态交叉。这个折中方案在实际项目中效果不错测试代码看起来更整洁也更容易追溯每个测试的依赖来源。6. 常见问题与避坑经验我把这些年踩过的雷集中写出来6.1 “when 不生效”或“打桩被忽略”的排查套路最经典的翻车现场明明when(mock.foo()).thenReturn(bar)写了执行被测代码后 foo() 返回的却是 null。排查思路按顺序来。第一检查打桩的参数匹配器。如果业务代码传了 null 而打桩用anyString()匹配不上直接返回默认 null。替换成nullable(String.class)看问题是否消失。第二确认被测代码里用的对象和你打桩的 mock 是同一个实例。Spring 容器注入的、反射创建的、工厂新建的对象都极有可能不是同一个实例这时打桩打到天上也没用。第三确认InjectMocks有没有成功注入。可以在测试里直接打印被测对象里依赖对象的 hashCode和 mock 的 hashCode 对比不一致就是注入失败。排查重构之后经常遇到这种问题改动类字段名后InjectMocks找不到对应名字静默失败。这么多排查路线里我最推荐先看“是不是同一个实例”因为 mock 不同实例这个问题在所有问题里占比最高尤其是构造函数中 new 出来的依赖mock 根本没法替换。老规矩被测类内部直接 new 的依赖要么改成构造器注入要么用 MockedConstruction否则测试怎么都会别扭。6.2 “MultipleMatchingStubs”和“UnnecessaryStubbingException”的含义这两个异常都是严格模式下的保护机制。UnnecessaryStubbingException意味着某根桩从打桩到测试结束都没有被调用代码里存在冗余。我的建议是直接删除多余打桩如果因为分支条件导致测试绕过了某根桩反而要审视测试是不是没有覆盖到预期分支。MultipleMatchingStubs相对更罕见表示同一方法同一参数匹配到了多条打桩规则Mockito 不确定该用哪个。出现这种错误时先检查是否有两个anyString()打桩了同一个方法再检查是否混用了精确匹配和模糊匹配。这类规则冲突不一定在运行时立刻报错等真正依赖到该方法的场景出现时失败信息会很模糊。平时保持打桩规则简洁不要为了“xx场景”一次性打满全部可能。6.3 验证失败却找不到调用日志可能是 void 方法或 lambda 内部调用有些公共代码在 lambda 里调 mock 对象因为 lambda 延迟执行验证时机可能在 lambda 真正执行前。这种情况下 verify 失败不一定意味着方法没被调用而是断言放在 lambda 执行之前。解决办法是确保被测代码完全执行比如调用 get() 或 join()、再执行 verify。如果是 lambda 里调用取得返回值后再断言注意执行顺序。这也解释了为什么异步代码的测试那么难写verify是同步的而内部逻辑是异步的竞态条件直接导致偶发失败。常见解法是引入Awaitility这类轮询等待库或把异步回调改成可同步执行的注入接口。7. 工具选型为什么是 Mockito 而不是其他方案7.1 Mockito 与 EasyMock、PowerMock 的取舍Mockito 并非唯一 mock 框架。EasyMock 是老牌框架它的 API 是“录制-回放-验证”流程先录制期望行为然后切换到 replay 状态执行被测代码最后验证。Mockito 用的是更轻量的“打桩即规则”模式不需要显式的 replay 切换测试流程更顺手学习曲线更低。从可读性角度看Mockito 的when(...).thenReturn(...)与自然语言接近更容易让不熟悉框架的人猜出含义。PowerMock 是老项目中为了解决静态方法、私有方法、构造函数 mock 而生的扩展在 Mockito 早期版本确实有必要。但随着 Mockito 逐步引入 MockedStatic、MockedConstruction、Spy 增强大部分 PowerMock 场景已经可以被 Mockito 直接覆盖。PowerMock 的强制字节码修改方案在 JDK 升级后经常遇到模块系统兼容问题所以新项目几乎没有理由选择 PowerMock。如果你正在维护老项目优先尝试把 PowerMock 的用法替换为 Mockito 原生方案能显著降低构建环境兼容风险。7.2 当依赖是“不容易 mock 的接口”时怎么办有些依赖是 final 类或者第三方库里没有接口、类方法还是 final 的。Mockito 对一个 final 类、final 方法的 mock 支持经历了变化——现代版本在绝大多数环境下已经能 mock final 类和方法原因是 Mockito 的 inline mock maker 通过字节码插桩绕过 final 限制。但配套要求是运行时允许字节码 agent 自附加 (-XX:EnableDynamicAgentLoading或对应 JDK 参数配置)。如果你的测试环境是 Java 17 且遇到 final mock 失败先检查 JVM 启动参数是否允许动态 agent 自带。另一种麻烦是“接口方法极大”的依赖比如一个接口有几十个方法而业务逻辑只用其中两个。Mockito mock 会为所有方法提供默认行为不需要你逐个打桩这是它好用的地方。真正麻烦的是那些在构造/初始化阶段就启动线程、连接资源的类mock 也无法阻止真实初始化。这种场景需要先重构依赖的初始化时机或者用mockConstruction绕过构造阶段。熟练了以后你会把这看成“设计可测性”的信号——如果一个类连测试都难以 mock它的依赖边界可能已经过度耦合了。7.3 团队协作中的 Mockito 规范单测代码的代码评审往往被轻视但 mock 用法的规范性直接影响测试可维护性。我在团队里推广过几条不成文的约定效果不错这里整理出来一个测试最多创建 3 个 mock多的 mock 往往意味着被测类承担了太多职责。不轻易使用verify(..., times(0))之外的弱断言弱断言会让回归漏洞从 CI 溜走。打桩规则不让 mock 返回“魔法值”比如1、abcd这类无业务含义的量最好用有语义的测试常量表示。所有 mock 资源尽量在测试方法内部或try-with-resources块内创建不用类级静态共享。这些规范不复杂但执行到位以后新人看老测试代码可以很快理解被测业务这是很大的隐性收益。8. 一段完整的“订单创建”示例从 mock 工具类到验证穿透前面分散讲了太多点最后放一个相对完整的例子把打桩、验证、异常处理、Spy 综合起来。假设我们要测试一个OrderService.createOrder方法基本信息如下调用 StockService.lockStock 锁定库存返回 boolean。调用 UserService.getUser 查询用户得到用户等级。调用 PaymentService.pay 执行扣款。如果库存不足直接抛 BizException。如果用户是 VIP订单标记加急。测试代码如下示意结构字段与细节按个人工程调整ExtendWith(MockitoExtension.class) class OrderServiceTest { Mock StockService stockService; Mock UserService userService; Mock PaymentService paymentService; InjectMocks OrderService orderService; Test void createOrder_whenStockNotEnough_shouldThrowBizException() { when(stockService.lockStock(sku_A, 10)).thenReturn(false); BizException e assertThrows(BizException.class, () - orderService.createOrder(new CreateOrderCmd(sku_A, 10))); assertEquals(库存不足, e.getMessage()); verify(paymentService, never()).pay(any()); verify(userService, never()).getUser(anyString()); } Test void createOrder_whenVipUser_shouldMarkAsUrgent() { when(stockService.lockStock(anyString(), anyInt())).thenReturn(true); when(userService.getUser(anyString())).thenReturn(vipUser()); when(paymentService.pay(anyString(), any(BigDecimal.class))).thenReturn(true); Order order orderService.createOrder(new CreateOrderCmd(sku_A, 10)); assertTrue(order.isUrgent()); verify(stockService).lockStock(sku_A, 10); verify(userService).getUser(anyString()); verify(paymentService).pay(sku_A, new BigDecimal(199.00)); } }第一个测试验证了失败的静默路径支付不被调用、用户不查询。第二个验证了成功路径及 VIP 加急语义。两个测试分别锁住库存失败分支和成功分支后续有人把库存逻辑改坏、把 VIP 判断去掉测试都会立刻报警。这就是“虚拟数据 交互验证”双线并用的直观体现桩数据控制分支走向验证断言控制行为合规。如果你对这个结构不满意还可以用ParameterizedTest扩展成多组参数覆盖普通用户、黑名单用户、金额为 0 等边界Mockito 和 JUnit 5 在这方面的协作非常顺手。最后分享一个个人习惯写完测试第一版后我会故意把被测代码里的某个关键 return 改成错误值、或者删掉某行调用看测试会不会变红。如果测试没有如预期报错说明这个测试的断言和打桩可能只是摆设。用这种方式定期“挑战”自己的测试代码比引入任何覆盖率门禁都更能保证单测质量。Mockito 给了你精确控制虚拟数据和验证交互的能力但最终测试值不值得信任仍然取决于你是否真的从“行为正确性”角度设计每一个 verify 和每一个 thenReturn。