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


原始标题:Mockito and Fluent APIs