Mockito and Fluent APIs

Fluent API 是一种基于方法链的软件工程设计技术,用于构建简洁、易读和健壮的接口。

它们通常用于建造者、工厂和其他创造性的设计模式。最近,随着 Java 的发展,它们变得越来越流行,并且可以在 Java Stream API 和 Mockito 测试框架等流行的 API 中找到。

然而,模拟 Fluent API 可能会很痛苦,因为我们经常需要设置一个复杂的模拟对象层次结构。

在本文中,我们将看看如何使用 Mockito 的强大功能来避免这种情况。

一个简单的 Fluent API

我们将使用构建器设计模式来说明用于构建披萨对象的简单流畅 API:

public class Pizza {
    public enum PizzaSize {
        LARGE, MEDIUM, SMALL;
    }

    private String name;
    private PizzaSize size;
    private List<String> toppings;
    private boolean stuffedCrust;
    private boolean collect;
    private Integer discount;

    private Pizza(PizzaBuilder builder) {
        this.name = builder.name;
        this.size = builder.size;
        this.toppings = builder.toppings;
        this.stuffedCrust = builder.stuffedCrust;
        this.collect = builder.collect;
        this.discount = builder.discount;
    }

    public String getName() {
        return name;
    }

    public PizzaSize getSize() {
        return size;
    }

    public List<String> getToppings() {
        return toppings;
    }

    public boolean isStuffedCrust() {
        return stuffedCrust;
    }

    public boolean isCollecting() {
        return collect;
    }

    public Integer getDiscount() {
        return discount;
    }

    public static class PizzaBuilder {
        private String name;
        private PizzaSize size;

        private List<String> toppings;
        private boolean stuffedCrust;
        private boolean collect;
        private Integer discount = null;

        public PizzaBuilder() {
        }

        public PizzaBuilder name(String name) {
            this.name = name;
            return this;
        }

        public PizzaBuilder size(PizzaSize size) {
            this.size = size;
            return this;
        }

        public PizzaBuilder withExtraTopping(String extraTopping) {
            if (this.toppings == null) {
                toppings = new ArrayList<>();
            }
            this.toppings.add(extraTopping);
            return this;
        }

        public PizzaBuilder withStuffedCrust(boolean stuffedCrust) {
            this.stuffedCrust = stuffedCrust;
            return this;
        }

        public PizzaBuilder willCollect(boolean collect) {
            this.collect = collect;
            return this;
        }

        public PizzaBuilder applyDiscount(Integer discount) {
            this.discount = discount;
            return this;
        }

        public Pizza build() {
            return new Pizza(this);
        }
    }
}

正如我们所见,我们创建了一个易于理解的 API,它读起来像 DSL,并允许我们创建具有各种特征的 Pizza 对象。

现在我们将定义一个使用我们的构建器的简单服务类。这将是我们稍后要测试的类:

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 的方法。顾名思义,我们可以使用这种方法来构建具有一些预定义属性的特殊披萨。

传统的Mock

以传统方式使用模拟存根将需要我们创建八个模拟 PizzaBuilder 对象。我们需要一个由 name 方法返回的 PizzaBuilder 的模拟,然后是一个由 size 方法返回的 PizzaBuilder 的模拟,等等。我们将继续这种方式,直到我们满足流式 API 链中的所有方法调用。

现在让我们看看我们如何编写一个单元测试来使用传统的 Mockito 模拟来测试我们的服务方法:

@RunWith(MockitoJUnitRunner.class)
public class PizzaServiceTest {

    @Mock
    private Pizza expectedPizza;

    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    private PizzaBuilder anotherbuilder;

    @Captor
    private ArgumentCaptor<String> stringCaptor;
    @Captor
    private ArgumentCaptor<Pizza.PizzaSize> sizeCaptor;

    @Test
    public void givenTraditonalMocking_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());

        // rest of test verification
    }

    @Test
    public void givenDeepStubs_whenServiceInvoked_thenPizzaIsBuilt() {
        Mockito.when(anotherbuilder.name(anyString())
                        .size(any(Pizza.PizzaSize.class))
                        .withExtraTopping(anyString())
                        .withStuffedCrust(anyBoolean())
                        .withExtraTopping(anyString())
                        .willCollect(anyBoolean())
                        .applyDiscount(anyInt())
                        .build())
                .thenReturn(expectedPizza);

        PizzaService service = new PizzaService(anotherbuilder);
        assertEquals("Expected Pizza", expectedPizza, service.orderHouseSpecial());
    }
}

在这个例子中,我们需要模拟我们提供给 PizzaService 的 PizzaBuilder。正如我们所看到的,这不是一项简单的任务,因为我们需要返回一个模拟,它将为我们流式 API 中的每个调用返回一个模拟。

这导致了复杂的模拟对象层次结构,难以理解并且难以维护。

要创建一个深度存根,我们只需在创建模拟时添加 Mockito.RETURNS_DEEP_STUBS 常量作为附加参数:

通过使用 Mockito.RETURNS_DEEP_STUBS 参数,我们告诉 Mockito 进行一种深度模拟。这使得可以一次性模拟完整方法链的结果,或者在我们的例子中是 fluent API。

值得庆幸的是,Mockito 提供了一个非常简洁的功能,称为深度存根,它允许我们在创建模拟时指定应答模式。

这导致了一个更优雅的解决方案和一个比我们在上一节中看到的更容易理解的测试。从本质上讲,我们避免了创建复杂的模拟对象层次结构的需要。

我们也可以直接通过@Mock 注解使用这种应答模式:

@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private PizzaBuilder anotherBuilder;

需要注意的一点是,验证仅适用于链中的最后一个模拟。

总结

在本文中,我们已经看到了如何使用 Mockito 来模拟一个简单的流式 API。首先,我们研究了一种传统的模拟方法,并了解了与这种方法相关的困难。

然后我们查看了一个使用 Mockito 鲜为人知的特性(称为深度存根)的示例,它允许以更优雅的方式模拟我们的流式 API。

版权声明:著作权归作者所有。

同意楼上的说法,收藏啦
这样的用法算是Mockito的高级用法了吧,收藏学习了
thumb_up 2 | star_outline 2 | textsms 2