Java单元测试利器Mockito:核心用法与团队实战规范 在做 Java 后端测试时Mockito 基本是一个绕不开的名字。它帮你造出虚拟依赖、控制外部调用返回值并精确验证对象之间的交互行为。这篇文章不会只贴几个示例凑数我会把 Mockito 从入门到进阶的核心用法、设计思路、踩坑经验一次性讲清楚包括参数匹配器、自定义 Answer、verify 验证、ArgumentCaptor、常见异常排查以及我们在真实项目里沉淀下来的 Mock 测试规范。无论你是刚开始接触单元测试还是已经在项目里用了一阵子 Mockito这篇内容都可以当作一份能直接参考的实战手册。1. 先从整体看 Mockito 要解决什么问题1.1 单元测试的痛点依赖地狱与不稳定测试我一直觉得很多团队不做单元测试不是不想做而是“做了就挂”。原因很典型被测的业务类里 new 了一个别的 Service那个 Service 又连 Redis、调第三方接口、读数据库。一旦本地环境没这些中间件测试直接失败。就算中间件存在每次跑测试还要等网络、等 IO慢得让人怀疑人生。Mockito 的解决方案是“替身”。它不启动容器、不连数据库而是通过动态代理或者子类方式在内存里生成一个目标接口或类的替身对象。被测类拿到的永远是替身而不是真实依赖。替身的所有方法默认什么都不做返回 null、false 或者 0然后你再通过打桩让它按测试需要返回指定结果。这样一来测试环境不再依赖中间件测试运行速度也能控制在毫秒级。但这里必须说清楚一个边界Mockito 是用来写单元测试的不是用来替代集成测试的。如果我们把数据库访问、Redis 缓存、外部 HTTP 全部 mock 掉那测的是“你自己的代码在理想环境下的逻辑”不是“组件协作的正确性”。所以我个人建议核心业务逻辑层用 Mockito 写单测涉及真实中间件的链路交给集成测试。你不需要为了 Mockito 而 mock 一切。1.2 Mockito 的三板斧mock、stub、verify 的定位与关系Mockito 的全部功能可以用三个词概括mock、stub、verify。三者各管一段组合起来才完整。mock造替身对象。它可以在不写实现类的情况下模拟一个接口或类的行为。stub打桩配置替身对象在特定参数下返回特定结果或者抛出指定异常。verify验证校验某方法是否被调用、调用次数、调用顺序和实际参数。它们的定位如果乱掉很容易写出意义不明的测试。我经常看到有人只 mock 不打桩结果对象返回 null断言全挂在 NPE 上也有人疯狂打桩但从不 verify根本不关心被测类到底调了谁。正确的心态应该是先用 mock 隔离外部依赖再用 stub 控制外部依赖的输出最后用 verify 保证被测类确实“做了该做的事”。这三步合起来才是 Mockito 能带给你的测试价值。1.3 什么时候不该用 MockitoMockito 不是银弹过度使用会反噬。用我经历过的一个小例子来说一个订单服务内部调用了库存服务扣减库存某次上线前单测全绿但联调时发现库存服务接口参数反了。原因就是单测里我把库存服务整个 mock 掉了根本没验证真实 HTTP 报文。这种测试看起来在隔离外部依赖实则把外部依赖的错误一起“隔离”了。所以我的判断标准是如果你 mock 的东西是“你自己团队写且逻辑复杂”的类请谨慎。它可能承担了重要业务规则mock 掉等于绕过了核心逻辑。如果你 mock 的东西是“跨进程、跨系统”的稳定边界比如第三方支付接口、消息队列客户端那非常合适。Mockito 是让测试变快的工具不是掩盖问题的遮羞布。2. 环境准备与最基础用法从 0 到 1 跑通 mock 和打桩2.1 引入依赖与初始化 mock 对象现在主流项目基本都是 Maven 或 Gradle。如果用的是 Mockito 5.x 和 JUnit 5Maven 里只需要两个依赖一个 mockito-core一个 mockito-junit-jupiter。Gradle 也一样不过顺手引入这两个依赖就行dependency groupIdorg.mockito/groupId artifactIdmockito-core/artifactId version5.11.0/version scopetest/scope /dependency dependency groupIdorg.mockito/groupId artifactIdmockito-junit-jupiter/artifactId version5.11.0/version scopetest/scope /dependency依赖引入后创建 mock 对象有两种方式。最简单的是直接静态调用mock()PaymentClient paymentClient mock(PaymentClient.class);另一种是配合注解我强烈推荐这种方式因为它让代码更干净ExtendWith(MockitoExtension.class) class OrderServiceTest { Mock PaymentClient paymentClient; Mock OrderRepository orderRepository; InjectMocks OrderService orderService; }这里ExtendWith(MockitoExtension.class)是 JUnit 5 的桥接器它负责在每条测试用例执行前初始化所有Mock注解字段。InjectMocks会把 mock 对象按构造方法参数注入到被测类里。很多新手忘了加ExtendWith或者MockitoAnnotations.openMocks(this)结果字段一直是 null测试一跑就 NPE排查半天。这个坑说小不小先记住。2.2 when().thenReturn() 打桩的正确姿势创建了 mock 对象默认情况下它的方法什么都不做。要让方法有返回值需要打桩。最经典的写法是when(paymentClient.pay(A001, new BigDecimal(99.00))) .thenReturn(true);这段代码翻译过来是当paymentClient.pay(A001, 99.00)被调用时返回true。被测类调用这段代码时不会真的发起网络请求而是直接拿到你配置的结果。这里有几个重要的细节。第一打桩的入参必须是 mock 对象的方法调用而不是被测对象的方法调用。我看过有人写when(orderService.create(...)).thenReturn(...)这在 Mockito 里是无效的因为它只能对 mock 对象生效。第二如果你不关心具体参数可以用参数匹配器比如when(paymentClient.pay(anyString(), any())).thenReturn(true)后面我会单独讲匹配器的坑。另外打桩的返回值要和真实业务逻辑匹配。比如pay方法本身可能返回一个布尔值但你打桩返回true测试里订单服务就会认为支付成功。如果你打桩返回 null订单服务可能直接 NPE。所以打桩不只是“让测试能跑”还在模拟真实依赖的合理输出。2.3 spy 的真实对象与部分打桩mock 是完全的替身但有时候我们需要“真实对象的大部分逻辑”只替换其中一小段。最典型的情况是测试一个类的私有方法之外的内部方法你不想跑真实的发邮件逻辑但希望类里其他方法正常执行。这时可以用spy。Spy EmailService emailService; Mock SmtpClient smtpClient; Test void testSendWelcomeEmail() { doReturn(true).when(emailService).sendInternal(anyString()); UserService userService new UserService(emailService); userService.welcome(张三); verify(emailService).sendInternal(张三); }这段代码里emailService是一个 spy 对象它默认会调用真实方法但sendInternal被显式打桩所以不会真的发出邮件。使用 spy 时有一点特别容易掉坑如果方法在 spy 内部被自调用比如sendWelcomeEmail方法内部调用了this.sendInternal那么打桩sendInternal是可以拦住的因为 Mockito 走的是子类代理。但如果方法被super.xxx()方式调用或者 final 方法、私有方法就拦不住了。遇到这种情况我会优先考虑重构代码把需要替换的依赖抽出来而不是强行对 spy 做文章。2.4 注解到底怎么配合 JUnit 使用使用注解时初始化顺序也算一个高频问题。InjectMocks会优先选构造方法注入如果没有构造方法则会用 setter 或字段注入。我个人建议被测类统一用构造方法注入并在测试类里把它写成显式创建而不是完全依赖InjectMocksBeforeEach void setUp() { orderService new OrderService(paymentClient, orderRepository); }显式构造有几个好处一是读者一眼能看清被测类依赖了哪些 mock二是避免InjectMocks在多个构造方法时“猜不透”而注入错对象三是测试里想传入一个不完全 mock 的依赖时灵活度高很多。InjectMocks本身很方便但我也见过它把我们“误导”得以为依赖都注入了实际却有字段漏掉。稳妥起见管理好依赖注入代码稍微多写两行完全值得。3. 参数匹配器与自定义 Answer让桩更聪明3.1 常用匹配器any、eq、matches 组合真实业务里方法调用很多是动态参数。比如订单号是一串随机生成的编号支付金额是计算出来的小数。如果用精确值打桩测试写起来会很痛苦因为你要提前知道所有运行时参数。这时参数匹配器就派上用场了。when(paymentClient.pay(anyString(), any(BigDecimal.class))) .thenReturn(true);anyString()匹配任意字符串any(BigDecimal.class)匹配任意 BigDecimal。更细一点的匹配器还包括anyInt()、anyLong()、contains(abc)、startsWith(订单)、matches(\\d{6})等。使用匹配器有一个铁律如果方法有多个参数要么全部用精确值要么全部用匹配器。不能写成when(obj.method(A001, anyInt()))因为 Mockito 在栈里记录了匹配器的位置你混用精确值会破坏它的内部状态直接抛InvalidUseOfMatchersException。这条我当年踩过后来成了团队评审必查项。// 这句话看起来“聪明”实际上会报错 when(paymentClient.pay(A001, any())) .thenReturn(true);正确做法是混用时用eq()包住精确值when(paymentClient.pay(eq(A001), any())) .thenReturn(true);这个eq()也是一个匹配器它表示“等于这个值”。只要配上eq上面的铁律就满足了。记住匹配器的世界里没有特殊的“精确值”一切都要用匹配器表达。3.2 自定义 Answer实现有逻辑的返回thenReturn只能返回固定值。但有些场景需要根据实际调用参数计算返回值比如根据折扣率算出最终支付金额或者根据用户类型返回不同结果。这时可以用thenAnswerwhen(promotionService.calculateDiscount(any(User.class))) .thenAnswer(invocation - { User user invocation.getArgument(0); if (VIP.equals(user.getLevel())) { return new BigDecimal(0.8); } return new BigDecimal(1.0); });thenAnswer接收一个Answer对象或者直接传 Lambda。InvocationOnMock里有getArgument()、getArguments()和getMethod()等方法你可以拿到本次调用的完整信息写任意计算逻辑。我曾经用它模拟过一个分布式锁连续调用 3 次返回成功之后返回失败以此测试并发场景下的降级逻辑。这个能力比写一堆 mock 状态字段干净多了。那thenReturn和thenAnswer怎么选固定值就thenReturn需要根据参数计算或需要模拟调用次数行为时用thenAnswer。两者的执行时机也不一样thenReturn是预先定义的返回值thenAnswer是调用时动态计算。性能上两者没有明显差距但thenAnswer灵活度更高也更容易写出过于复杂的逻辑注意别把测试写成了另一个业务实现。3.3 doReturn/when 与 void 方法打桩有些方法没有返回值比如eventBus.publish(event)、logger.log(msg)。这种 void 方法不能直接when(...).thenReturn(...)因为when里的表达式本身需要评估一个返回值而 void 方法返回 void 根本无法继续调用thenReturn。这时候要换doXxx().when()系列doNothing().when(eventBus).publish(any(OrderEvent.class)); doThrow(new RuntimeException(发送失败)) .when(eventBus) .publish(any(OrderEvent.class)); doAnswer(invocation - { // 模拟异步回调 return null; }).when(eventBus).publish(any(OrderEvent.class));doReturn().when()也可以用来给普通方法打桩它的效果和when().thenReturn()基本一致但执行时机不同when().thenReturn()是在调用时触发而doReturn().when()是先配置后触发。对于会执行真实方法而导致抛异常的情况比如 spy 对象doReturn().when()更保险。反过来when().thenReturn()有编译期类型检查的优势能提前发现返回值类型不匹配。两条路没有绝对优劣我个人的习惯是普通方法用when().thenReturn()void、spy 和需要避免真实调用的场景用doReturn().when()。4. 行为验证verify 与 ArgumentCaptor4.1 验证调用次数与顺序mock 的另一个价值是行为验证。verify可以确认被测对象是否真的调用了某个依赖以及调用是否满足预期次数。最常见的是times()verify(orderRepository, times(1)).save(any(Order.class)); verify(paymentClient, never()).refund(anyString()); verify(paymentClient, atLeastOnce()).pay(anyString(), any()); verify(paymentClient, atMost(3)).pay(anyString(), any());times(1)是最常用的但如果业务里存在循环、重试机制次数断言能帮大忙。比如要求“失败后最多重试 2 次”你可以在测试里模拟第一次失败、第二次成功然后verify(paymentClient, times(2)).pay(...)。这种验证能直接暴露重试逻辑的缺陷。如果还要验证调用顺序用InOrderInOrder inOrder inOrder(paymentClient, orderRepository); inOrder.verify(paymentClient).pay(anyString(), any()); inOrder.verify(orderRepository).save(any(Order.class));这个场景对应的是“先支付再保存订单”的业务规则。顺序验证往往被忽略但一旦你的业务对时序有要求它是最能表达意图的断言方式。注意inOrder里要列出所有涉及的 mock 对象不然 Mockito 无法构建全局顺序。4.2 捕获参数ArgumentCaptor 的用法verify还能配合ArgumentCaptor捕获被测类传给依赖的实际参数然后对参数内容做断言。相比用匹配器“匹配一个值”直接通过捕获参数能让你检查更细致的字段。ArgumentCaptorOrder orderCaptor ArgumentCaptor.forClass(Order.class); verify(orderRepository).save(orderCaptor.capture()); Order savedOrder orderCaptor.getValue(); assertEquals(A001, savedOrder.getOrderId()); assertEquals(new BigDecimal(99.00), savedOrder.getAmount());这段代码的意义在于我们不只要确认save被调用了还要确认保存的订单里金额是否经过正确计算、订单状态是否被置为“已支付”。如果被测类里有一行order.setStatus(PAYED)忘写了thenReturn打桩不会暴露它ArgumentCaptor却可以立刻抓住。需要捕获多个参数时可以用captor.getAllValues()拿到本次测试里所有调用传入的值列表适合测试循环批量保存的场景。4.3 verifyNoMoreInteractions 与 verifyNoInteractions 的取舍Mockito 还提供了两个“清场”式验证verifyNoMoreInteractions(mock)验证 mock 上没有多余的未验证调用verifyNoInteractions(mock)验证 mock 完全没有任何交互。很多人看到这俩挺兴奋觉得能保证“被测代码没有额外动作”结果在真实项目里堆了一堆verifyNoMoreInteractions每加一行业务代码就开始红测试变得出奇地脆弱。我比较克制。verifyNoInteractions适合断言“某个依赖完全不该被触碰”的场景比如库存不足时不应该调用支付客户端when(inventoryService.checkStock(anyString())).thenReturn(false); assertThrows(BusinessException.class, () - orderService.create(A001)); verifyNoInteractions(paymentClient);但verifyNoMoreInteractions的问题是它对“验证过哪些方法”有严格顺序要求。如果你前面用verify只验证了pay后面被测类还调用了getStatus这个getStatus也会被算作“多余交互”测试直接挂。这种约束收益低、维护成本高我会尽量避免放在老代码改造里只在全新代码且预先定义好交互契约时使用。5. 常见问题与排查技巧实录5.1 InvalidUseOfMatchersException 为什么老缠着你这是 Mockito 新手最常遇到的异常没有之一。字面意思是“匹配器使用非法”但真实原因通常有两种。第一种是匹配器和精确值混用上文已经提到。第二种是匹配器出现在了不该出现的地方比如把any()写在测试方法里的某个中间变量上或者写在一个自定义方法里而非直接作为 mock 方法参数// 错误写法 String s anyString(); when(paymentClient.pay(s, any())).thenReturn(true);这里anyString()在when外面执行Mockito 的匹配器栈记录不到对应上下文自然报错。排查技巧也很直接看异常堆栈第一行它通常会指出哪个测试方法、哪一行出问题优先检查最近一次when/verify的参数是否全用了匹配器。如果你在循环里调用带匹配器的方法也要注意循环结束后匹配器状态是否被清理干净。实际上Mockito 会在下一步操作时自动清空匹配器栈所以循环里使用匹配器本身没问题但循环里如果混用了精确值就会引发连锁异常。5.2 静态方法不能 mock 和 final 类问题传统 JVM 代理只能 mock 非 final 的实例方法和接口。因此很多人一直以为 Mockito 无法 mock 静态方法或 final 类。从 Mockito 4 开始内置了 inline mock maker可以 mock 静态方法、final 类和 final 方法。但我必须强调能用是能用不建议当常规武器。静态方法 mock 的核心风险在两个方面一是 mock 静态方法要确保测试是单线程的、没有并发干扰因为它改的是整个类加载器的字节码二是测试代码和真实代码的耦合度会变高一旦有人改了静态方法签名测试里所有相关打桩都要变。我在项目里见过大量MockedStatic嵌套在 try-with-resources 里的写法可读性很差。如果想把静态调用干掉我更推荐先做代码重构把静态方法包一层实例对象或者用一个可替换的间接层。重构之后测试自然干净不会沦为 Mockito 特技表演。5.3 thenReturn 与 doReturn 搞混会踩什么坑这两者的区别前面说过但踩坑实录值得再强调一次。如果你 mock 的对象是一个 super 类或者接口代理when().thenReturn()在配置阶段会先执行一次真实调用对 spy 而言。这个“先执行”有时候会引发副作用。例如spyUserService.getUser(A001)在真实环境会查数据库你用when(spyUserService.getUser(A001)).thenReturn(fakeUser)时Mockito 为了计算 stubbing真的会把getUser(A001)调用一次数据库直接报错。解决办法就是把打桩顺序反过来用doReturn(fakeUser).when(spyUserService).getUser(A001)。它不会在配置阶段触发真实方法。另一个常见点如果thenReturn传的参数类型不对编译器能帮我们拦截但doReturn是 Object 类型没有编译期类型检查如果返回值类型不匹配会运行到被测代码时才 ClassCastException。所以我的习惯是能用when().thenReturn()就优先用spy 和 void 场景再用doReturn().when()。5.4 排查技巧失败测试先看哪几样当一个 Mockito 测试失败时很多人上来就 debug效率极低。我的排查顺序固定成四步。第一步看断言错误里哪个 mock 方法“被期望调用却没有调用”这通常是打桩条件不对或参数匹配器写错第二步看是否有 NPE这多半是 mock 对象没初始化检查ExtendWith和InjectMocks是否存在第三步看是否有UnnecessaryStubbingException说明你打桩了但测试路径根本没走到那一步可能被测逻辑分支不对也可能是过度打桩第四步再往上翻异常堆栈确认是不是在 spy 或静态方法 mock 时触发了真实调用。这套顺序在实际排查中省了我大量时间。有些开发一看到异常就怀疑框架 bug其实 Mockito 异常信息写得已经很直白错误原因和提示修复方向通常就在第一行异常里。静下心读异常往往比 debug 一个个断点快得多。我顺手整理了一个速查表供参考异常现象常见原因处理方案InvalidUseOfMatchersException匹配器和精确值混用用 eq() 包住精确值大量 NPEmock 未初始化加 ExtendWith(MockitoExtension.class) 或 openMocksUnnecessaryStubbingException打桩未被执行删除未用打桩或用 lenient()真实方法被调用spywhen().thenReturn 配置阶段触发真实调用改用 doReturn().when()ClassCastExceptiondoReturn 返回值类型不匹配检查返回值类型尽量用 when().thenReturn() 获得编译期检查MockitoException: static mocking未启用 inline mock maker确认 Mockito 5.x 内置或引入 mockito-inline6. 实际项目中的工作流与团队规范6.1 写 mock 代码的团队约定Mockito 用久了很容易写出“全 mock、全覆盖、全 verify”的测试看着绿实际是纸老虎。我们团队慢慢沉淀了几条规则我觉得值得分享。第一条对外部依赖做 mock对内部逻辑不做 mock。说白了就是只替身“别人的对象”不替身“自己的核心业务”第二条每条测试只验证一个核心行为。如果一个测试方法里有三个 Verify、五个 when它已经变成集成测试了维护成本远比收益高第三条打桩无用的桩要清理。Mockito 4 以后默认对未使用的桩报UnnecessaryStubbingException这不是框架找茬而是帮你发现测试用例没走到对应分支。如果确实准备了一个桩但这条分支没走到说明测试意图不明建议拆分成两个用例。另外命名上尽量沿着“行为”而非“实现”走。比如shouldThrowExceptionWhenStockNotEnough就比testCreateOrderFail更能表达意图。配合这些约定的测试才能作为业务行为的活文档而不是一堆只知道“跑绿”的验证器。6.2 用 Mockito 重构一段“难测代码”的实例有一个模拟项目我记忆很深。老代码里订单模块直接在方法内部new PaymentClient()导致测试没法替换每次单测都会真的去创建连接。我用 Mockito 重构它的思路分三步第一步把PaymentClient抽成构造参数让依赖从内部创建变外部注入第二步在测试类里 mockPaymentClient并用InjectMocks注入被测服务第三步对测试中原先无法触达的逻辑分支补齐验证。重构之后原来三分钟跑不完的测试变成了一秒内跑完而且还顺手发现了一个老 bug订单金额为 0 时依然调用了支付接口。这类问题在旧代码里几乎不可能通过手工测试暴露出来但重构后加一个verify(paymentClient, never()).pay(...)就牢牢锁住了。可以说Mockito 倒逼了一次更健康的依赖注入设计这种收益往往比 mock 本身更大。6.3 我最后想分享的一个小技巧关于 Mockito我最后想补充一个不那么热门但非常实用的小技巧用BDDMockito风格的 API 提高测试可读性。它把when(...).thenReturn(...)写成given(...).willReturn(...)把verify(...)写成then(...).should(...)。这听上去只是换了个名字但对团队协作帮助很大因为given让测试读起来像“安排场景”then让断言看起来像“验证结果”业务人员也能大致看懂测试在干什么。// BDD 风格 given(paymentClient.pay(anyString(), any())) .willReturn(true); orderService.create(A001, new BigDecimal(99.00)); then(orderRepository).should(times(1)).save(any(Order.class)); then(paymentClient).should(never()).refund(anyString());当然这只是风格层的改变底层一切都还是 Mockito。如果你所在的团队正在从零建设单元测试我建议直接统一成 BDD 风格因为它更贴近“行为驱动”的思考路径团队评审时也更高效。等大家形成了写测试的肌肉记忆再慢慢挖掘 Mockito 更高级的特性也不迟。