Mockito and Fluent APIs – Mockito和Fluent APIs

最后修改: 2019年 11月 22日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

Fluent APIs are a software engineering design technique based on method chaining for building concise, readable and eloquent interfaces.

流畅的API是一种基于方法链的软件工程设计技术,用于构建简明、可读和雄辩的界面。

They’re often used for builders, factories and other creational design patterns. In recent times they’ve become increasingly popular with the evolution of Java and can be found in popular APIs such as the Java Stream API and Mockito testing framework.

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

Nevertheless, mocking Fluent APIs can be painful as we often need to set up a complex hierarchy of mock objects.

尽管如此,嘲弄Fluent API可能会很痛苦,因为我们经常需要建立一个复杂的嘲弄对象的层次结构

In this tutorial, we’ll take a look at how we can avoid this using a great feature of Mockito.

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

2. A Simple Fluent API

2.一个简单的流畅的API

In this tutorial, we’ll use the builder design pattern to illustrate a simple fluent API for constructing a pizza object:

在本教程中,我们将使用构建器设计模式来说明构建比萨对象的简单流畅的API

Pizza pizza = new Pizza
  .PizzaBuilder("Margherita")
  .size(PizzaSize.LARGE)
  .withExtaTopping("Mushroom")
  .withStuffedCrust(false)
  .willCollect(true)
  .applyDiscount(20)
  .build();

As we can see, we’ve created an easy to understand API which reads like a DSL and allows us to create a Pizza object with various characteristics.

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

Now we’ll define a simple service class that uses our builder. This will be the class we’re going to test later on:

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

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

Our service is pretty simple and contains one method called orderHouseSpecial. As the name implies, we can use this method to build a special pizza with some predefined properties.

我们的服务相当简单,包含一个名为orderHouseSpecial的方法。顾名思义,我们可以用这个方法来建立一个具有一些预定义属性的特殊比萨饼。

3. Traditional Mocking

3.传统的嘲弄

Stubbing with mocks in the traditional way is going to require that we create eight mock PizzaBuilder objects. We’ll need a mock for the PizzaBuilder returned by the name method, then a mock for the PizzaBuilder returned by the size method, etc. We’ll continue in this fashion until we satisfy all the method calls in our fluent API chain.

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

Let’s now take a look at how we might write a unit test to test our service method using conventional Mockito mocks:

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

@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
}

In this example, we need to mock the PizzaBuilder that we supply to the PizzaService. As we can see, this is no trivial task as we need to return a mock, which will return a mock for each call in our fluent API.

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

This leads to a complicated hierarchy of mock objects which is difficult to understand and can be tricky to maintain.

这导致了复杂的模拟对象层次结构,很难理解,而且维护起来也很麻烦。

4. Deep Stubbing to the Rescue

4.深耕细作的救援

Thankfully, Mockito provides a really neat feature called deep stubbing which allows us to specify an Answer mode when we create a mock.

庆幸的是,Mockito提供了一个非常整洁的功能,叫做deep stubbing,它允许我们在创建一个模拟时指定一个Answer模式。

To make a deep stub, we simply add the Mockito.RETURNS_DEEP_STUBS constant as an additional argument when we create a mock:

要制作一个深度存根,我们只需在创建一个模拟时将Mockito.RETURNS_DEEP_STUBS常量作为一个额外的参数。

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

By using the Mockito.RETURNS_DEEP_STUBS argument, we tell Mockito to make a kind of deep mock. This makes it possible to mock the result of a complete method chain or in our case fluent API in one go.

通过使用Mockito.RETURNS_DEEP_STUBS参数,我们告诉Mockito做一种深度模拟。这使得我们可以一次性地模拟一个完整的方法链的结果,或者在我们的案例中模拟流畅的API。

This leads to a much more elegant solution and a test that is much easier to understand than the one we saw in the previous section. In essence, we avoid the need to create a complex hierarchy of mock objects.

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

We can also use this answer mode directly with the @Mock annotation:

我们也可以直接用@Mock注解来使用这种回答模式。

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

One point to note is that verification will only work with the last mock in the chain.

需要注意的一点是,验证将只对链中的最后一个mock发挥作用。

5. Conclusion

5.结论

In this quick tutorial, we’ve seen how we can use Mockito to mock a simple fluent API. First, we looked at a traditional mocking approach and understood the difficulties associated with this method.

在这个快速教程中,我们已经看到了如何使用Mockito来模拟一个简单的流畅的API。首先,我们看了一个传统的嘲讽方法,并了解了与这种方法有关的困难。

Then we looked at an example using a little known feature of Mockito called deep stubs which permits a more elegant way to mock our fluent APIs.

然后我们看了一个使用Mockito的一个鲜为人知的功能的例子,这个功能被称为 “深度存根”,它允许以一种更优雅的方式来模拟我们的流畅的API。

As always, the full source code of the article is available over on GitHub.

一如既往,文章的完整源代码可在GitHub上获得