1. 概述
流式 API(Fluent API)是一种基于方法链(method chaining)的设计模式,旨在构建简洁、易读且富有表达力的接口。
这类 API 在构建器(Builder)、工厂等创建型设计模式中非常常见。随着 Java 8 的普及,流式风格越来越流行,比如我们熟悉的 Java Stream API 和 Mockito 测试框架本身,都大量使用了这种模式。
✅ 然而,在单元测试中模拟(mock)流式 API 却是个经典“踩坑”点——你往往需要为链式调用中的每一环都创建 mock 对象,导致测试代码臃肿不堪、难以维护。
本文将介绍如何利用 Mockito 的一个强大特性——deep stubbing(深度桩),简单粗暴地解决这个问题。
2. 一个简单的流式 API 示例
我们以 Pizza 构建器为例,展示一个典型的流式 API:
Pizza pizza = new Pizza
.PizzaBuilder("Margherita")
.size(PizzaSize.LARGE)
.withExtaTopping("Mushroom")
.withStuffedCrust(false)
.willCollect(true)
.applyDiscount(20)
.build();
这段代码读起来就像一门领域特定语言(DSL),清晰表达了披萨的构建过程。
接下来,我们定义一个使用该构建器的服务类,也就是我们后续要测试的目标:
public class PizzaService {
private Pizza.PizzaBuilder builder;
public PizzaService(Pizza.PizzaBuilder builder) {
this.builder = builder;
}
public Pizza orderHouseSpecial() {
return builder.name("Special")
.size(PizzaSize.LARGE)
.withExtraTopping("Mushrooms")
.withStuffedCrust(true)
.withExtraTopping("Chilli")
.willCollect(true)
.applyDiscount(20)
.build();
}
}
orderHouseSpecial()
方法用于创建一份“招牌披萨”,内部通过流式调用完成构建。
3. 传统 Mock 方式:繁琐且易错
如果用传统方式 mock 这个流式调用,你需要为链中的每一个方法返回值都创建一个 mock 对象。
⚠️ 换句话说,为了支持 7 次链式调用,你得手动 mock 7 个 PizzaBuilder
实例,形成一个 mock 调用链。
来看一个典型的“灾难现场”:
@Test
public void givenTradittonalMocking_whenServiceInvoked_thenPizzaIsBuilt() {
PizzaBuilder nameBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
PizzaBuilder sizeBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
PizzaBuilder firstToppingBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
PizzaBuilder secondToppingBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
PizzaBuilder stuffedBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
PizzaBuilder willCollectBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
PizzaBuilder discountBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
PizzaBuilder builder = Mockito.mock(Pizza.PizzaBuilder.class);
when(builder.name(anyString())).thenReturn(nameBuilder);
when(nameBuilder.size(any(Pizza.PizzaSize.class))).thenReturn(sizeBuilder);
when(sizeBuilder.withExtraTopping(anyString())).thenReturn(firstToppingBuilder);
when(firstToppingBuilder.withStuffedCrust(anyBoolean())).thenReturn(stuffedBuilder);
when(stuffedBuilder.withExtraTopping(anyString())).thenReturn(secondToppingBuilder);
when(secondToppingBuilder.willCollect(anyBoolean())).thenReturn(willCollectBuilder);
when(willCollectBuilder.applyDiscount(anyInt())).thenReturn(discountBuilder);
when(discountBuilder.build()).thenReturn(expectedPizza);
PizzaService service = new PizzaService(builder);
Pizza pizza = service.orderHouseSpecial();
assertEquals("Expected Pizza", expectedPizza, pizza);
verify(builder).name(stringCaptor.capture());
assertEquals("Pizza name: ", "Special", stringCaptor.getValue());
}
❌ 问题很明显:
- 代码量爆炸,7 层调用就得 7 个 mock
- 难读、难写、难维护
- 一旦链式顺序或方法变动,测试就得重写
4. 使用 Deep Stubs:一招制敌
Mockito 提供了一个叫 deep stubbing 的特性,可以让你直接 mock 一整条方法链,无需手动构造中间 mock 对象。
✅ 核心用法:RETURNS_DEEP_STUBS
创建 mock 时传入 Mockito.RETURNS_DEEP_STUBS
,Mockito 会自动为链式调用中的每一环返回一个“虚拟”对象(deep stub)。
@Test
public void givenDeepMocks_whenServiceInvoked_thenPizzaIsBuilt() {
PizzaBuilder builder = Mockito.mock(Pizza.PizzaBuilder.class, Mockito.RETURNS_DEEP_STUBS);
Mockito.when(builder.name(anyString())
.size(any(Pizza.PizzaSize.class))
.withExtraTopping(anyString())
.withStuffedCrust(anyBoolean())
.withExtraTopping(anyString())
.willCollect(anyBoolean())
.applyDiscount(anyInt())
.build())
.thenReturn(expectedPizza);
PizzaService service = new PizzaService(builder);
Pizza pizza = service.orderHouseSpecial();
assertEquals("Expected Pizza", expectedPizza, pizza);
}
关键优势:
- ✅ 一行
when()
搞定整个链式调用 - ✅ 不再需要手动创建中间 mock 对象
- ✅ 测试代码简洁、直观、易维护
注解方式(推荐)
你也可以在字段上直接使用注解,更简洁:
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private PizzaBuilder builder;
搭配 @ExtendWith(MockitoExtension.class)
使用,清爽又专业。
⚠️ 注意事项
- 验证(verify)只能作用于链的最后一个方法。比如你无法
verify(builder).size(...)
,因为中间对象是自动生成的桩。 - deep stubs 适合用于返回值链式调用,不适合用于有副作用的方法链(比如每一步都在修改状态)。
5. 总结
- 流式 API 虽然写起来爽,但传统 mock 方式测试起来非常痛苦。
- Mockito 的
RETURNS_DEEP_STUBS
特性可以大幅简化对链式调用的模拟,避免构建复杂的 mock 层级。 - 推荐在测试构建器、DSL 等流式接口时优先使用 deep stubs,提升测试可读性和维护性。
- 记住:verify 只对链尾有效,设计测试时注意规避。
示例代码已上传至 GitHub:https://github.com/yourname/mockito-fluent-api-demo