Mockito单元测试实战:从核心方法到Spring Boot落地全解析
2026/9/9 20:00:28 网站建设 项目流程

写 Mockito 相关的实战总结,是我一直想认真做的一件事。这框架在 Java 单元测试里几乎是标配,但很多人只是停留在mock()when().thenReturn()的层面,遇到真实的 Spring Boot 项目、复杂的依赖注入、静态方法、异步调用就不知道怎么处理了。这篇就围绕 Mockito 在单元测试里的完整落地展开,从核心概念到实践经验,把值得注意的细节都过一遍。

1. 思路拆解:单元测试为何离不开 Mockito

1.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);

方式二:注解 +@RunWithMockitoExtension。在 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、ArgumentCaptor

Mockito 的核心方法数量并不多,但每个都值得深入理解。我按使用频率整理成一张表:

方法用途典型场景
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! UserId=1001"); }

更进一步,如果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()被调用了,还想确认事件里的orderIdamount正确。这时就用ArgumentCaptor

@Test 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); ArgumentCaptor<OrderCreatedEvent> 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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>

spring-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是否正确包装并向上抛出。

这些场景里,OrderRepositoryEventPublisher都是 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); ArgumentCaptor<Order> orderCaptor = ArgumentCaptor.forClass(Order.class); verify(orderRepository).save(orderCaptor.capture()); assertEquals(0, orderCaptor.getValue().getAmount().compareTo(amount)); assertEquals("CREATED", orderCaptor.getValue().getStatus()); ArgumentCaptor<OrderCreatedEvent> 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.4@InjectMocks注入失败的常见场景与排查

@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> <groupId>org.mockito</groupId> <artifactId>mockito-inline</artifactId> <scope>test</scope> </dependency>

Spring Boot 3.x 如果用的 Mockito 5.x,mockito-inline已经集成在核心模块里了,不需要额外引。这个变化很多人没注意到,导致老项目换新版本后报Mockito cannot mock this class之类的错误。

有了依赖之后,静态方法的 mock 逻辑如下,比如某个工具类:

public class IdGenerator { public static Long nextId() { // 真实实现,可能依赖数据库序列 return System.currentTimeMillis(); } }

测试里用MockedStatic来管理静态方法 mock 的生命周期:

@Test void shouldUseMockedStaticIdGenerator() { try (MockedStatic<IdGenerator> 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.xmlmaven-surefire-plugin里配置:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.2.5</version> <configuration> <argLine> --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED </argLine> </configuration> </plugin>

这个配置不是所有项目都需要,但遇到诡异 mock 创建失败时,可以加上试试。

4.3 私有方法的测试三大流派

私有方法能不能测、该不该测,是老生常谈的问题。我的观点是:如果你发现需要测私有方法才能覆盖某些分支,这个私有方法的逻辑大概率应该被提取成一个独立的公开方法或者独立的类。测试私有方法本身是一个“代码坏味道”的信号。

但如果确实需要,有三种流派:

流派一:反射调用。这是最原始的方式,用ReflectionTestUtils(Spring 提供)或者纯 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 风格 API
when(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-inline
UnnecessaryStubbingException测试里定义了打桩但没使用删除没用的 when 语句,或在类上加@MockitoSettings(strictness = Strictness.LENIENT)
Argument mismatch报错使用了无法序列化的参数匹配器检查是否混合使用原始值和匹配器,所有参数都要用匹配器包裹
InvalidUseOfMatchersExceptionwhen 语句里匹配器用法不对mock 方法的每个参数要么全部用匹配器,要么全部用具体值,不能混
静态方法 mock 影响其他测试MockedStatic 没正确关闭使用 try-with-resources 或在@AfterEach里关闭
测试单独跑通过,全部跑失败测试之间状态泄漏检查静态 mock、ThreadLocal、Spring 上下文缓存
@InjectMocks注入为 null构造函数多个或字段不匹配手动构造对象,不依赖 @InjectMocks
mock 对象实际调用了真实方法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> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.11</version> <executions> <execution> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>verify</phase> <goals> <goal>report</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 verifytest阶段只执行单元测试,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 不是一个什么神秘的框架,它就是帮你把“测试”这件事变得简单了一点、快了一点、安全了一点。而这“一点”,足以帮你挡住后面无数的线上问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询