写 Mockito 相关的实战总结是我一直想认真做的一件事。这框架在 Java 单元测试里几乎是标配但很多人只是停留在mock()和when().thenReturn()的层面遇到真实的 Spring Boot 项目、复杂的依赖注入、静态方法、异步调用就不知道怎么处理了。这篇就围绕 Mockito 在单元测试里的完整落地展开从核心概念到实践经验把值得注意的细节都过一遍。1. 思路拆解单元测试为何离不开 Mockito1.1 没有 Mockito 之前单元测试有多痛写单元测试的人都有这种体验被测对象往往不“干净”。比如一个订单服务OrderService它要调用OrderRepository查数据库要调UserClient拿用户信息可能还要发 MQ 消息通知。你想验证OrderService.createOrder()的逻辑结果测试一旦跑起来要么连不上数据库要么调外部接口超时要么 MQ 没启动直接报错。测试还没写完光搭环境就劝退一大半人。之前我见过不少项目所谓的“单元测试”其实是启动整个 Spring Boot 容器连上测试库用内嵌 Redis、MQ 模拟器整套跑一遍。这种方式不是不行但它更接近集成测试。一旦项目变大这种测试的耗时越来越长稳定性越来越差跑一次全量测试要二十分钟开发根本不愿意在本地跑CI 上也经常因为环境问题飘红。单元测试的初衷是快速反馈、精确定位问题这类“重测试”完全背离了这个初衷。1.2 Mockito 的核心价值隔离依赖专注逻辑Mockito 解决的就是“隔离”这件事。通过动态代理在内存中生成一个“模拟替身”把被测类依赖的外部组件全部替换掉。你不需要真实的数据库、真实的 HTTP 服务、真实的消息队列只需要告诉 Mockito当调用某个方法时返回什么结果或者期望它被调用过几次。打个比方你想测试一个厨师做菜的水平不需要真的去菜市场买菜、开火、备齐所有食材。你只需要准备几份模拟食材mock 对象告诉厨师这些食材是什么味道然后观察他做出来的菜对不对。Mockito 就是那个模拟食材供应商让你能专注于厨师本身的刀工和火候。这个思路的价值体现在几个方面测试运行快。没有 IO、没有网络请求纯内存执行几千个测试用例几十秒跑完。测试稳定。外部服务挂了不影响单测结果CI 上的随机失败率大幅下降。定位精准。测试只针对当前类的逻辑失败时能明确知道是这一段逻辑的问题而不是被外部环境干扰。1.3 适用场景与选择边界Mockito 适合绝大多数 Java 项目的单元测试无论是纯 Java 应用、Spring Boot 项目、Android 开发还是其他基于 JVM 的框架比如 Solon、若依这类国产脚手架核心用法是一致的。但也要清楚它的边界。Mockito 不是一个“万能测试工具”以下场景不适合硬用数据库 ORM 映射、SQL 语法正确性验证应该用 Testcontainers 或者 H2 做集成测试。第三方 SDK 的真实行为验证比如真实的支付回调、OAuth 授权流程应该用 Mock Server 或沙箱环境。浏览器端的交互逻辑应该用 Selenium 或 Playwright。并发性能测试用 JMeter 或 Gatling。一句话单元测试用 Mockito 保证逻辑正确集成测试用真实组件保证协作正确两者互补不冲突。2. 核心机制与基础实战从 mock 对象到行为验证2.1 创建 Mock 对象的三种方式Mockito 使用上首先就是创建 mock 对象。常见的有三种方式我建议根据项目场景选择。方式一静态方法创建。这是最基础的方式任何时候都能用UserMapper userMapper mock(UserMapper.class);方式二注解 RunWith或MockitoExtension。在 JUnit 4 里配合MockitoJUnitRunner在 JUnit 5 里配合MockitoExtension这也是我推荐的主流方式ExtendWith(MockitoExtension.class) class UserServiceTest { Mock private UserMapper userMapper; InjectMocks private UserService userService; }方式三MockitoAnnotations.openMocks(this)手动初始化。这个适用于某些特殊场景比如测试类有复杂的父类继承结构不方便直接用扩展的时候。三种方式的选择逻辑很简单能用注解就用注解代码整洁可读性强需要在静态方法或特定工具类里临时 mock就用静态方法遇到兼容性问题再手动初始化。2.2 核心方法全解析when、doReturn、verify、ArgumentCaptorMockito 的核心方法数量并不多但每个都值得深入理解。我按使用频率整理成一张表方法用途典型场景when(...).thenReturn(...)给方法调用打桩返回指定值模拟查询接口返回固定数据when(...).thenThrow(...)打桩并抛出异常模拟依赖方法运行时抛错doReturn(...).when(...)对 void 方法或 spy 对象打桩模拟无返回值方法的副作用doThrow(...).when(...)让 void 方法抛异常模拟发送消息失败verify(...)验证某个方法是否被调用确认关键方法被调用过verify(...).times(2)验证调用次数确认循环逻辑次数正确verifyNoMoreInteractions(...)验证没有多余交互确认对象没有额外方法调用ArgumentCaptor捕获方法入参捕获对象并断言内部属性when().thenReturn()和doReturn().when()的选择是个常见困惑点。我的经验是对于有返回值的方法优先用when().thenReturn()语法更直观对于 void 方法或者需要避免真实调用的情况用doReturn().when()。比如doReturn(user).when(userMapper).selectById(1L)这种写法即使是 spy 对象部分 mock也能避免真正执行方法体。2.3 verify 的精髓不只是“调用了”更是“调得对”verify 是许多人容易忽略的功能。刚开始写测试时我也只关心“返回结果对不对”后来才意识到有些方法没有返回值或者返回值对象里只有部分信息这时候验证“方法有没有被正确调用”反而比验证返回值更重要。举个例子一个短信发送服务public class NotificationService { private final SmsClient smsClient; public NotificationService(SmsClient smsClient) { this.smsClient smsClient; } public void sendWelcomeMessage(Long userId, String phone) { String content Welcome! UserId userId; smsClient.send(phone, content); } }测试里如果只验证方法不抛异常等于没测。正确的做法是验证smsClient.send()被调用过并且入参正确Test void shouldSendWelcomeMessage() { NotificationService service new NotificationService(smsClient); service.sendWelcomeMessage(1001L, 13800138000); verify(smsClient).send(13800138000, Welcome! UserId1001); }更进一步如果SmsClient的入参是一个对象那就需要ArgumentCaptor来捕获并断言内部字段了。这个在 2.4 里重点讲。2.4 ArgumentCaptor 的实战价值ArgumentCaptor用来捕获传入 mock 方法的参数然后对参数做进一步断言。它是测试“协作正确性”的最强工具之一。看一个例子订单服务创建订单后会发布一个事件public class OrderService { private final OrderRepository orderRepository; private final EventPublisher eventPublisher; public OrderService(OrderRepository orderRepository, EventPublisher eventPublisher) { this.orderRepository orderRepository; this.eventPublisher eventPublisher; } public Long createOrder(BigDecimal amount) { Order order new Order(); order.setAmount(amount); order.setStatus(CREATED); Order saved orderRepository.save(order); eventPublisher.publish(new OrderCreatedEvent(saved.getId(), saved.getAmount())); return saved.getId(); } }测试时你不仅想知道eventPublisher.publish()被调用了还想确认事件里的orderId和amount正确。这时就用ArgumentCaptorTest void shouldPublishOrderCreatedEvent() { Order savedOrder new Order(); savedOrder.setId(999L); savedOrder.setAmount(new BigDecimal(199.99)); when(orderRepository.save(any(Order.class))).thenReturn(savedOrder); Long orderId orderService.createOrder(new BigDecimal(199.99)); assertEquals(999L, orderId); ArgumentCaptorOrderCreatedEvent captor ArgumentCaptor.forClass(OrderCreatedEvent.class); verify(eventPublisher).publish(captor.capture()); OrderCreatedEvent event captor.getValue(); assertEquals(999L, event.getOrderId()); assertEquals(new BigDecimal(199.99), event.getAmount()); }ArgumentCaptor在实际工作里最有价值的场景是当你重构代码把一个方法拆成多个方法或者把参数从基本类型改成包装对象时它能直接帮你验证重构前后行为是否一致相当于行为层面的回归保障。3. 完整实战Spring Boot 项目中用 Mockito 写出高质量单测3.1 项目背景与依赖准备以一个常见的 Spring Boot 3.x 项目为例测试订单服务的完整流程。先确认测试依赖都加齐了。Maven 项目的pom.xml里至少要有dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependencyspring-boot-starter-test里已经包含 JUnit 5、Mockito、AssertJ 等常用测试库不需要额外引入 Mockito 依赖。Gradle 项目则对应testImplementation org.springframework.boot:spring-boot-starter-test这里有个容易忽略的坑Spring Boot 2.1 以后默认用 JUnit 5但有些老项目还在用 JUnit 4两者在注解上完全不同。JUnit 4 用RunWith(MockitoJUnitRunner.class)JUnit 5 用ExtendWith(MockitoExtension.class)混用会直接报错或注解不生效。所以第一步先确认项目里 JUnit 版本避免后面所有测试都踩坑。3.2 “该测什么、不该测什么”测试设计原则写测试之前最重要的一件事是想清楚边界。拿OrderService.createOrder()来说它本身不写 SQL、不发 HTTP只负责编排流程。测试目标应该锁定在输入合法时是否调用了orderRepository.save()并传入了正确的订单对象。输入金额为负数时是否抛出IllegalArgumentException。当订单保存成功后是否发布了OrderCreatedEvent事件。当orderRepository.save()抛出数据库异常时OrderService是否正确包装并向上抛出。这些场景里OrderRepository、EventPublisher都是 mock 对象。如果你在单元测试里用真实的OrderRepository那就变成集成测试了应该放到SpringBootTest那一层去覆盖。这是个很重要的理念单元测试追求“一个测试类只验证一个类的逻辑”如果OrderService的测试里同时验证了OrderRepository的逻辑那这个测试类就干了两个类的活职责混乱了。3.3 实战代码完整的 Test 类下面给一个可以直接抄作业的测试类覆盖了正常流程、异常流程、边界值验证和参数捕获ExtendWith(MockitoExtension.class) class OrderServiceTest { Mock private OrderRepository orderRepository; Mock private EventPublisher eventPublisher; InjectMocks private OrderService orderService; Test void createOrder_whenAmountValid_shouldSaveAndPublishEvent() { BigDecimal amount new BigDecimal(99.00); Order order new Order(); order.setId(1L); order.setAmount(amount); order.setStatus(CREATED); when(orderRepository.save(any(Order.class))).thenReturn(order); Long orderId orderService.createOrder(amount); assertNotNull(orderId); assertEquals(1L, orderId); ArgumentCaptorOrder orderCaptor ArgumentCaptor.forClass(Order.class); verify(orderRepository).save(orderCaptor.capture()); assertEquals(0, orderCaptor.getValue().getAmount().compareTo(amount)); assertEquals(CREATED, orderCaptor.getValue().getStatus()); ArgumentCaptorOrderCreatedEvent eventCaptor ArgumentCaptor.forClass(OrderCreatedEvent.class); verify(eventPublisher).publish(eventCaptor.capture()); assertEquals(1L, eventCaptor.getValue().getOrderId()); } Test void createOrder_whenAmountNegative_shouldThrowException() { BigDecimal negativeAmount new BigDecimal(-10.00); assertThrows(IllegalArgumentException.class, () - orderService.createOrder(negativeAmount)); verify(orderRepository, never()).save(any()); verify(eventPublisher, never()).publish(any()); } Test void createOrder_whenSaveThrowsException_shouldWrapAndRethrow() { when(orderRepository.save(any(Order.class))) .thenThrow(new DataAccessException(db down) {}); assertThrows(OrderCreationException.class, () - orderService.createOrder(new BigDecimal(50.00))); } }这段代码里几个关键点我再展开说明InjectMocks负责把上面Mock出来的两个对象注入到OrderService的构造函数里。它的注入策略依次是构造函数注入、Setter 注入、字段注入。对于现代 Spring Bean 都推荐用构造函数注入InjectMocks也能识别。any(Order.class)是 Mockito 内置的参数匹配器表示“任意 Order 对象”。如果不加参数匹配器直接写when(orderRepository.save(order)).thenReturn(...)那就要求传入的必须是同一个对象实例这在很多场景下是做不到的。never()是验证次数的反向断言表示方法一次都没被调用。这类断言在异常分支里特别有用能确认代码在出错时没有继续往下执行。3.4InjectMocks注入失败的常见场景与排查InjectMocks用起来方便但偶尔会静默失败。最典型的情况是OrderService有多个构造函数Mockito 选择不了该用哪个。public class OrderService { private final OrderRepository orderRepository; private final EventPublisher eventPublisher; public OrderService(OrderRepository orderRepository) { this(orderRepository, new NoopEventPublisher()); } public OrderService(OrderRepository orderRepository, EventPublisher eventPublisher) { this.orderRepository orderRepository; this.eventPublisher eventPublisher; } }这个类有两个构造函数Mockito 在注入时可能选择参数更少的那个导致eventPublisher为 null测试执行到orderService.createOrder()时直接 NPE。解决办法有两个方案一测试里直接手动构造OrderService不依赖InjectMocks最稳妥BeforeEach void setUp() { orderService new OrderService(orderRepository, eventPublisher); }方案二删除多余构造函数让依赖关系更清晰。这也倒逼业务代码保持单一构造函数的设计。我在团队里更提倡方案二。构造函数多了不仅让InjectMocks困惑也容易让业务代码出现“可空依赖”的隐患。4. 高难度场景实战静态方法、final 方法、私有方法4.1 如何 mock 静态方法mockito-inline 详解Mockito 3.4.0 开始支持 mock 静态方法但需要额外的mockito-inline依赖。在 Spring Boot 2.x 项目里默认引入的 Mockito 可能不带这个支持需要在pom.xml里显式添加dependency groupIdorg.mockito/groupId artifactIdmockito-inline/artifactId scopetest/scope /dependencySpring Boot 3.x 如果用的 Mockito 5.xmockito-inline已经集成在核心模块里了不需要额外引。这个变化很多人没注意到导致老项目换新版本后报Mockito cannot mock this class之类的错误。有了依赖之后静态方法的 mock 逻辑如下比如某个工具类public class IdGenerator { public static Long nextId() { // 真实实现可能依赖数据库序列 return System.currentTimeMillis(); } }测试里用MockedStatic来管理静态方法 mock 的生命周期Test void shouldUseMockedStaticIdGenerator() { try (MockedStaticIdGenerator mocked mockStatic(IdGenerator.class)) { mocked.when(IdGenerator::nextId).thenReturn(10086L); OrderService service new OrderService(orderRepository, eventPublisher); service.setIdGenerator(IdGenerator.class); Long id service.generateOrderId(); assertEquals(10086L, id); mocked.verify(IdGenerator::nextId, times(1)); } }注意MockedStatic要放进 try-with-resources 里否则静态 mock 会泄漏到其他测试用例导致诡异的跨测试影响。这个坑我踩过不止一次排查方向通常是某个测试单独跑通过全部一起跑就报错多半就是静态 mock 没有正确关闭。4.2 mock final 类和 final 方法Mockito 2.x 以后已经默认支持 mock final 类和方法不需要额外配置。真正的坑在于有些项目用了 Byte Buddy 在某些 JDK 上版本不一致导致创建 mock 时抛异常。如果遇到Mockito cannot mock/spy because : final class或类似报错优先检查Mockito 版本是否低于 2.x如果是升级。是否用了mockito-core被旧版 Byte Buddy 冲突尝试统一版本。是否有自定义的 ClassLoader 干扰。尤其在 Java 17 上模块系统限制可能导致需要加--add-opens参数。Java 17 是很多团队现在的主流版本我建议在pom.xml的maven-surefire-plugin里配置plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId version3.2.5/version configuration argLine --add-opens java.base/java.langALL-UNNAMED --add-opens java.base/java.utilALL-UNNAMED /argLine /configuration /plugin这个配置不是所有项目都需要但遇到诡异 mock 创建失败时可以加上试试。4.3 私有方法的测试三大流派私有方法能不能测、该不该测是老生常谈的问题。我的观点是如果你发现需要测私有方法才能覆盖某些分支这个私有方法的逻辑大概率应该被提取成一个独立的公开方法或者独立的类。测试私有方法本身是一个“代码坏味道”的信号。但如果确实需要有三种流派流派一反射调用。这是最原始的方式用ReflectionTestUtilsSpring 提供或者纯 Java 反射。优点是直接缺点是测试写起来啰嗦重构时容易断。流派二把私有方法改为包级私有或 protected然后在同包测试类里直接调用。这个方式改动小但污染了类的访问控制。流派三通过公开方法的路径间接覆盖私有方法逻辑。这是我最推荐的方式。比如一个私有方法validateOrder(Order order)它内部的逻辑会在createOrder()执行流程中被调用测试构造合法的、非法的订单去调用createOrder()就能覆盖到私有分支。public class OrderService { public Long createOrder(Order order) { validateOrder(order); // 私有方法在公开流程里被调用 return orderRepository.save(order).getId(); } private void validateOrder(Order order) { if (order.getAmount() null || order.getAmount().compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(amount must be positive); } } }测试时只需要测试createOrder(nullAmount)会抛异常即可不需要单独测validateOrder。4.4 spy 的用法真实对象与 mock 行为的混合体spy是 Mockito 里一个容易被忽略但非常好用的功能。它创建的是“真实对象 部分打桩”的组合体。默认调用真实方法但你可以对指定方法打桩。场景有一个现成的UserService里面有个方法getFullUserInfo()会调用getBaseUserInfo()和getExtraInfo()。你只想 mock 掉耗时的getExtraInfo()保留getBaseUserInfo()的真实逻辑这时用 spy 最合适。Test void shouldSpyPartialMethod() { UserService spyService spy(new UserService()); when(spyService.getExtraInfo()).thenReturn(extra data); String result spyService.getFullUserInfo(); assertTrue(result.contains(base)); assertTrue(result.contains(extra data)); }注意 spy 使用时的经典陷阱如果getBaseUserInfo()内部调用了getExtraInfo()你用when(spyService.getExtraInfo()).thenReturn(...)打桩在getFullUserInfo()被调用时真实方法内部调的是this.getExtraInfo()还是spyService.getExtraInfo()答案取决于调用方式。如果真实方法内部用的是this.xxx()那 mock 不生效如果用spyService.xxx()才会走 mock。所以 spy 更适用于“对象外部调用方法”的场景内部自调用时行为会出乎意料。这个细节在写测试时很容易被忽略踩过坑的人会懂。5. BDDMockito、Answer 自定义与常见问题排查5.1 BDDMockito让测试更像行为描述BDDMockito 是 Mockito 提供的 BDD 风格 API它和经典 API 的对应关系如下经典 APIBDD 风格 APIwhen(x).thenReturn(y)given(x).willReturn(y)when(x).thenThrow(e)given(x).willThrow(e)verify(x).method()then(x).should().method()verify(x, times(2)).method()then(x).should(times(2)).method()BDD 风格的优势是测试的可读性更强尤其是在大型项目里测试代码本身就是文档。比如Test void createOrderShouldReturnOrderId() { // given Order order new Order(); order.setAmount(new BigDecimal(66.00)); given(orderRepository.save(any(Order.class))).willReturn(order); // when Long id orderService.createOrder(new BigDecimal(66.00)); // then assertThat(id).isEqualTo(1L); then(eventPublisher).should().publish(any(OrderCreatedEvent.class)); }使用 BDDMockito 并不需要额外依赖org.mockito.BDDMockito和 AssertJ 配合使用体验极佳。如果你的团队还没有统一的单测风格我建议考虑一下这种写法。5.2 Answer 自定义动态决定返回值thenReturn只能返回固定的值但有些场景需要根据参数动态计算返回值。这时用Answer接口。比如一个批量查询接口要根据传入的 ID 返回对应的用户when(userMapper.findById(anyLong())).thenAnswer(invocation - { Long id invocation.getArgument(0); User user new User(); user.setId(id); user.setName(User- id); return user; }); User user userService.getUser(1L); assertEquals(User-1, user.getName());Answer更高级的用法是模拟数据库自增 ID 行为AtomicLong counter new AtomicLong(); when(orderRepository.save(any(Order.class))).thenAnswer(invocation - { Order order invocation.getArgument(0); order.setId(counter.incrementAndGet()); return order; });这段代码模拟了“保存订单后自动分配 ID”的行为非常贴近真实数据库的效果而又不需要真实数据库。5.3 常见问题速查表我把实际工作中遇到的高频问题整理成一张速查表方便大家遇到时直接对照。问题现象可能原因解决方案Mockito cannot mock this classMockito 版本太老不支持 final 类/方法升级 Mockito 或加 mockito-inlineUnnecessaryStubbingException测试里定义了打桩但没使用删除没用的 when 语句或在类上加MockitoSettings(strictness Strictness.LENIENT)Argument mismatch报错使用了无法序列化的参数匹配器检查是否混合使用原始值和匹配器所有参数都要用匹配器包裹InvalidUseOfMatchersExceptionwhen 语句里匹配器用法不对mock 方法的每个参数要么全部用匹配器要么全部用具体值不能混静态方法 mock 影响其他测试MockedStatic 没正确关闭使用 try-with-resources 或在AfterEach里关闭测试单独跑通过全部跑失败测试之间状态泄漏检查静态 mock、ThreadLocal、Spring 上下文缓存InjectMocks注入为 null构造函数多个或字段不匹配手动构造对象不依赖 InjectMocksmock 对象实际调用了真实方法spy 或注解使用错误检查是否用 spy 误打桩改用 doReturn().when()关于UnnecessaryStubbingException这是 Mockito 的严格模式默认行为。很多新手第一次遇到会很懵明明测试能过为什么报错其实这是 Mockito 在帮你检查测试质量如果你打了一个桩但测试流程里没用上说明这个桩是多余的要么是测试没走完预期路径要么是测试本身有冗余代码。如果你想临时关闭严格模式可以MockitoSettings(strictness Strictness.LENIENT) class OrderServiceTest { }但从长期维护角度我更建议保留严格模式让它帮你发现测试中的冗余。5.4 提升测试代码质量的实用技巧几个从实践中沉淀出来的经验第一测试方法命名要带场景和预期。不要写testCreateOrder()这种写createOrder_whenAmountNegative_shouldThrowException()这种“方法名_条件_预期结果”三段式命名。好处是测试失败时从报告里就能判断哪里出了问题。第二断言要用领域语言。能用 AssertJ 的assertThat(order.getAmount()).isEqualByComparingTo(99.00)就不要用 JUnit 的assertEquals。AssertJ 的错误提示更友好链式调用更易读。第三测试数据不要随便造。我见过很多测试里写new Order()然后不设置任何属性最后为了满足某个非空校验在代码里加了一堆 if 判断。这是本末倒置。测试数据应该尽量模拟真实场景比如金额要用new BigDecimal(199.99)而不是new BigDecimal(1)字符串字段要有业务含义。第四一个测试方法只验证一个行为。如果一个方法里同时断言了保存成功、发布事件、返回正确 ID 这三件事那当测试失败时你只知道“createOrder 有问题”但不知道是保存环节、事件环节还是返回环节的问题。拆成多个测试方法每个只验证一个行为失败时定位成本会低很多。第五用DisplayName补充中文描述。虽然方法名已经表达了意图但在测试报告里中文描述更直观。比如Test DisplayName(金额为负数时抛出异常) void createOrder_whenAmountNegative_shouldThrowException() { }这个习惯在团队协作时特别好用产品经理和测试同事也能看懂自动化测试在覆盖什么。6. 工具链整合与团队落地建议6.1 Mockito 与 JaCoCo 覆盖率工具的配合Mockito 解决了“怎么写测试”的问题JaCoCo 解决“测了多少”的问题。两者配合才能看清楚单测的覆盖情况。在 Maven 项目里用 JaCoCo 插件在验证阶段生成覆盖率报告plugin groupIdorg.jacoco/groupId artifactIdjacoco-maven-plugin/artifactId version0.8.11/version executions execution goals goalprepare-agent/goal /goals /execution execution idreport/id phaseverify/phase goals goalreport/goal /goals /execution /executions /plugin执行mvn verify后会在target/site/jacoco/index.html生成覆盖率报告。我建议团队把核心业务模块的覆盖率目标定在 80% 以上但不要盲目追求 100%。100% 覆盖率往往意味着大量测试是在测 getter/setter 或者无意义的分支边际价值很低。覆盖率真正有价值的观察维度是“分支覆盖率”不是“行覆盖率”。行覆盖只能证明这行执行了分支覆盖才能证明 if/else 的各种路径都被走到了。Mockito 配合分支覆盖率统计能更好发现漏测的边界条件。6.2 Mockito 在 CI 流水线中的集成实践本地测试跑通了团队协作还需要 CI 的守护。在 GitLab CI、GitHub Actions、Jenkins 中通常的做法是在 push 或 PR 阶段执行mvn test。如果测试失败流水线直接中断。这里有一个实践小技巧CI 上不要只跑mvn test建议跑mvn verify。test阶段只执行单元测试verify阶段会顺便执行集成测试和覆盖率检查。如果你的集成测试比较重不希望每次 push 都跑可以单独建一个 nightly 流水线跑完整验证日常 push 只跑mvn test。Mockito 测试在 CI 上的稳定性非常关键。前面提到的“测试单独跑通过全部跑失败”的问题在 CI 上会被放大因为 CI 通常是全量跑测试。所以建议本地提交前也执行一次全量测试。6.3 团队落地时怎么推广引入 Mockito 的难点通常不在技术而在习惯。要让团队真正用好 Mockito我觉得有几件事值得做第一明确测试优先级。新写的业务代码必须配套单元测试最好能做到“先写测试再写实现”或者至少“写完代码立刻写测试”。提交代码前检查测试覆盖率不达标不放行。第二建立测试代码评审规范。代码审查时不仅要看业务代码也要看测试代码。测试代码有问题往往比业务代码有问题更隐蔽它会让团队产生虚假的安全感——测试跑过了实际上什么都没验证。第三定期清理慢测试和无效测试。有些测试因为历史原因变得非常慢比如不该用 Spring Boot 的测试用了SpringBootTest严重影响团队信心最终大家跑都不想跑。这时候要把这些重测试摘出去该用 Mockito 轻量替换的就替换掉让单测回归“秒级反馈”的体验。第四从实际痛点入手。不要一上来就翻遍所有项目去写测试而是从当前最经常出 bug 的模块开始。比如订单金额计算、权限判断、状态流转这类核心逻辑先把它们用 Mockito 保护起来团队看到价值后自然就会跟进。6.4 一段关于测试理念的总结性思考写到这里想分享一个我个人比较深的体会。很多人把 Mockito 当成一个“糊弄测试”的工具用when().thenReturn()把所有依赖都 mock 掉然后测试里全是“返回了一个假数据验证了一些无关紧要的逻辑”。这种测试写再多也只是在制造一种“我们在做单元测试”的假象。Mockito 的真正价值在于逼你去思考“我的类到底依赖了什么、协作关系是什么”。当你写verify(orderRepository).save(orderCaptor.capture())时你其实是在确认“我的类和仓储层的协作是保存一个订单对象”。当你写doThrow(new DataAccessException(...)).when(orderRepository).save(any())时你是在确认“依赖的异常传递机制是什么”。这些思考本身就是对设计质量的一次审视。从实际项目中的数据来看Mockito 单测覆盖到位后很多低级 bug 会在本地开发阶段就暴露而不是等到测试环境甚至生产环境才发现。修复一个本地单测发现的 bug和修复一个线上事故的成本差距是数量级的。这也是我一直强调团队要“认真写单测”的根本原因。我的建议是从今天开始从一个没有测试的类入手先用Mock把它的依赖隔离出来写三个测试——正常逻辑、异常分支、边界条件。跑通之后你会感受到Mockito 不是一个什么神秘的框架它就是帮你把“测试”这件事变得简单了一点、快了一点、安全了一点。而这“一点”足以帮你挡住后面无数的线上问题。
