导读:本期聚焦于弥生美月创作的《Spring Boot 单元测试中如何用 Mockito 模拟依赖?常见用法与避坑实践》,敬请观看详情。单元测试里最头疼的事情莫过于被测类依赖了一堆外部服务:数据库、第三方接口、消息队列,任何一个不模拟都会让测试又慢又脆。Mockito 正是解决这个问题的利器,它能在运行时生成替身对象,让测试聚焦在业务逻辑本身。本文围绕 Spring Boot 场景展开,先讲清 Mockito 的核心注解与打桩方法,再对比 @Mock 与 @MockBean 的适用边界,配合 when、verify、ArgumentCaptor 等常用 API 的代码示例,说明如何覆盖正常与异常分支,最后整理参数匹配器的坑、打桩失效等高频踩坑点,帮助读者写出稳定可靠的单元测试。

写单元测试时,被测类往往依赖 Repository、远程服务或者消息组件,如果直接把这些真实依赖拉进测试,轻则测试跑得飞快变慢,重则因为环境不通导致用例时红时绿。Mockito 的价值就在这里:它可以在运行时替你“伪造”这些依赖,让测试只关心当前类的逻辑对不对。本文结合 Spring Boot 的实际用法,把 Mockito 的核心机制、常用 API 和容易踩的坑一次讲清楚。

Spring Boot 单元测试中如何用 Mockito 模拟依赖?常见用法与避坑实践

一、Mockito 的核心机制与注解用法

Mockito 的本质是利用动态代理在内存中生成一个实现了目标接口(或继承了目标类)的替身对象。这个对象的所有方法默认都返回 null、0 或者 false,不会产生任何真实的副作用。当你在测试里调用被测类的方法时,被测类内部对依赖的调用实际上落到了这个替身上,从而实现了测试范围的隔离。

在单元测试中使用 Mockito 最常见的方式是配合 @ExtendWith(MockitoExtension.class) 注解,这是 JUnit 5 的写法。配合 @Mock 生成模拟对象,配合 @InjectMocks 把这些模拟对象注入到被测类的构造方法或字段中,一个最小可用的测试骨架如下:

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private PaymentClient paymentClient;

    @InjectMocks
    private OrderService orderService;

    @Test
    void should_create_order_successfully() {
        // 打桩:模拟支付接口返回成功
        when(paymentClient.pay(anyString())).thenReturn(true);
        when(orderRepository.save(any(Order.class)))
                .thenAnswer(inv -> inv.getArgument(0));

        Order order = orderService.createOrder("SKU-1001", 2);

        assertNotNull(order.getId());
        // 验证 save 方法被调用了一次
        verify(orderRepository, times(1)).save(any(Order.class));
    }
}

需要注意的是,@InjectMocks 的注入规则是“尽力而为”:它会优先尝试构造器注入,其次是属性注入,最后是 setter 注入。如果被测类有多个构造器或者字段类型重复,注入结果可能不符合预期,此时建议显式在 @BeforeEach 里手动构造被测对象,语义更清晰也更稳定。

二、@Mock 与 @MockBean 的区别与选择

Spring Boot 开发者最容易混淆的就是这两个注解。它们名字相近,但背后的机制完全不同。@Mock 是 Mockito 原生注解,生成的对象与 Spring 容器毫无关系,测试根本不会启动 Spring 上下文,速度快,适合纯单元测试。@MockBean 则是 Spring Boot Test 提供的注解,它会启动 Spring 容器,并把容器中对应类型的 Bean 替换成 Mockito 的模拟对象,适合需要容器参与的切片测试或集成测试。

两者的差异可以用下面这张表概括:

对比项@Mock@MockBean
来源Mockito 原生Spring Boot Test
是否启动 Spring 容器
执行速度毫秒级秒级(首次启动)
替换容器 Bean不会
适用场景纯逻辑单元测试切片测试、集成测试

一个常见的误区是在所有测试里无脑使用 @MockBean。由于每次使用 @MockBean 都可能改变上下文配置,Spring 会为新测试类重建 ApplicationContext,导致缓存失效、测试整体变慢。如果只是验证 Service 层的业务逻辑,完全不需要容器,直接用 @Mock@InjectMocks 即可。只有当你需要 @WebMvcTest@SpringBootTest 环境,又想让某个外部依赖不落库、不发真实请求时,才考虑 @MockBean

@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private OrderService orderService;

    @Test
    void should_return_order_detail() throws Exception {
        given(orderService.findById(1L)).willReturn(new Order(1L, "SKU-1001"));

        mockMvc.perform(get("/api/orders/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.sku").value("SKU-1001"));
    }
}

三、打桩与验证:when、verify 和 ArgumentCaptor

打桩(stubbing)是告诉模拟对象“当某个方法被调用时返回什么”的过程,核心 API 是 when(...).thenReturn(...)。除了固定返回值,thenAnswer 可以根据入参动态计算结果,thenThrow 可以模拟异常分支,thenDoNothing 用于让 void 方法静默通过。覆盖异常分支是很多团队容易遗漏的地方,比如支付接口超时的场景:

@Test
void should_throw_when_payment_timeout() {
    when(paymentClient.pay(anyString()))
            .thenThrow(new PaymentTimeoutException("connect timeout"));

    assertThrows(PaymentTimeoutException.class,
            () -> orderService.createOrder("SKU-1001", 2));

    // 异常路径下不应该保存订单
    verify(orderRepository, never()).save(any());
}

验证(verify)用来断言“某个方法是否被调用、被调用了几次、参数是什么”。除了 timesnever,还有 atLeastOnceatMost 等模式。当需要检查传给依赖的完整对象内容时,ArgumentCaptor 是最趁手的工具,它可以把方法调用时实际传入的参数捕获下来,再对字段逐个断言:

@Test
void should_save_order_with_correct_fields() {
    when(paymentClient.pay(anyString())).thenReturn(true);

    orderService.createOrder("SKU-1001", 2);

    ArgumentCaptor<Order> captor = ArgumentCaptor.forClass(Order.class);
    verify(orderRepository).save(captor.capture());

    Order saved = captor.getValue();
    assertEquals("SKU-1001", saved.getSku());
    assertEquals(2, saved.getQuantity());
    assertEquals(OrderStatus.PAID, saved.getStatus());
}

四、高频踩坑点与最佳实践

第一个坑是参数匹配器的混用问题。一旦某个参数使用了 any()eq() 这类匹配器,同一次调用中的其他参数也必须使用匹配器,不能裸写普通值,否则会抛出 InvalidUseOfMatchersException。正确做法是把普通值包一层 eq(),例如 when(client.query(any(), eq(10))).thenReturn(list)

第二个坑是对 mock 出来的类打桩失效。Mockito 默认不能模拟 final 类和 final 方法(3.4 版本后需引入 mockito-inline 才支持),如果你的依赖是 final 类,打桩会静默不生效,方法仍走真实逻辑。遇到这种情况,要么引入 inline mock maker,要么重新审视设计是否应该面向接口编程。

第三个坑是过度模拟。当测试里把所有依赖都 mock 掉,甚至连简单的值对象也 mock,测试就会变得非常脆弱——被测代码只要稍微重构,测试就大面积报红。一个实用的判断标准是:mock 那些跨越进程边界或涉及 I/O 的依赖(数据库、HTTP、MQ),而值对象、工具类、纯函数直接用真实实现。

最后是可读性建议。测试方法名用 should_xxx_when_xxx 的格式表达意图;每个测试只验证一个行为;必要时使用 BDDMockitogiven(...).willReturn(...) 风格,让测试读起来更像需求描述。BDD 风格与 when 在功能上完全等价,纯粹是团队口味问题,但统一风格比选择哪种风格更重要。掌握这些原则后,配合 CI 中强制执行的测试覆盖率门禁,Mockito 就能真正成为保障代码质量的基础设施,而不是摆设。

MockitoSpring Boot单元测试Mock Bean修改时间:2026-09-08 21:27:07

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260908/52997.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。