Mockito和流式API

2023/05/12

1. 概述

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

它们通常用于建造者、工厂和其他创建型的设计模式。 近年来,随着Java的发展,它们变得越来越流行,并且可以在很多地方看到它们的出现,例如Java Stream API和Mockito测试框架。

然而,mock Fluent API可能会很痛苦,因为我们经常需要建立mock对象的复杂层次结构

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

2. 一个简单的Fluent API

在本文中,我们将使用建造者设计模式演示一个用于构建Pizza对象的简单fluent API:

public class Pizza {
    private final String name;
    private final PizzaSize size;
    private final List<String> toppings;
    private final boolean stuffedCrust;
    private final boolean collect;
    private final 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;
    }

    // getters and setters ...

    public enum PizzaSize {
        LARGE, MEDIUM, SMALL
    }

    public static class PizzaBuilder {
        private String name;
        private PizzaSize size;
        private List<String> toppings;
        private boolean stuffedCrust;
        private boolean collect;
        private Integer discount = null;
        // build methods ...
    }
}

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

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

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();
    }
}

我们的Service非常简单,包含一个名为orderHouseSpecial()的方法。顾名思义,我们可以使用这个方法来构建具有一些预定义属性的特殊Pizza。

3. 传统mock

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

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


@ExtendWith(MockitoExtension.class)
class PizzaServiceUnitTest {
    @Mock
    private Pizza expectedPizza;

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

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

    @Test
    void givenTraditionalMocking_whenServiceInvoked_thenPizzaIsBuilt() {
        Pizza.PizzaBuilder nameBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
        Pizza.PizzaBuilder sizeBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
        Pizza.PizzaBuilder firstToppingBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
        Pizza.PizzaBuilder secondToppingBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
        Pizza.PizzaBuilder stuffedBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
        Pizza.PizzaBuilder willCollectBuilder = Mockito.mock(Pizza.PizzaBuilder.class);
        Pizza.PizzaBuilder discountBuilder = Mockito.mock(Pizza.PizzaBuilder.class);

        Pizza.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);
        assertEquals(expectedPizza, service.orderHouseSpecial(), "Expected Pizza");

        verify(builder).name(stringCaptor.capture());
        assertEquals("Pizza name: ", "Special", stringCaptor.getValue());

        verify(nameBuilder).size(sizeCaptor.capture());
        assertEquals(Pizza.PizzaSize.LARGE, sizeCaptor.getValue(), "Pizza size: ");
    }
}

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

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

4. Deep Stubbing

值得庆幸的是,Mockito提供了一个非常简洁的功能,称为deep stubbing,它允许我们在创建mock时指定Answer

要创建一个deep stubbing,我们只需在创建mock时添加Mockito.RETURNS_DEEP_STUBS常量作为附加参数:

class PizzaServiceUnitTest {

    @Test
    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(expectedPizza, service.orderHouseSpecial(), "Expected Pizza");
    }
}

通过使用Mockito.RETURNS_DEEP_STUBS参数,我们告诉Mockito进行一种deep mock,这使得可以一次性mock完整方法链的结果。

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

我们也可以直接通过@Mock注解使用这种Answer mode:

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

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

5. 总结

在这个教程中,我们看到了如何使用Mockito来mock一个简单的流式API。首先,我们给出了一种传统的mock方法,演示了它的缺点。

然后我们给出一个使用Mockito中deep stubbing的示例,它允许以更优雅的方式mock我们的流式API。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章