«1. Введение

Fluent API — это метод разработки программного обеспечения, основанный на цепочке методов для создания кратких, удобочитаемых и красноречивых интерфейсов.

Они часто используются для строителей, фабрик и других творческих шаблонов проектирования. В последнее время они становятся все более популярными по мере развития Java, и их можно найти в популярных API, таких как Java Stream API и среда тестирования Mockito.

Тем не менее, имитация API Fluent может быть болезненной, поскольку нам часто нужно настроить сложную иерархию фиктивных объектов.

В этом уроке мы рассмотрим, как этого избежать, используя замечательную функцию Mockito.

2. Простой Fluent API

В этом руководстве мы будем использовать шаблон проектирования Builder, чтобы проиллюстрировать простой Fluent API для создания объекта пиццы:

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

Как мы видим, мы создал простой для понимания 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. Как следует из названия, мы можем использовать этот метод для создания специальной пиццы с некоторыми предопределенными свойствами.

3. Традиционное мокирование

Традиционное заглушение мок-объектов потребует создания восьми фиктивных объектов PizzaBuilder. Нам понадобится мокап для PizzaBuilder, возвращаемый методом name, затем макет для PizzaBuilder, возвращаемый методом size, и т. д. Мы будем продолжать в том же духе, пока не удовлетворим все вызовы методов в нашей цепочке API Fluent.

Давайте теперь посмотрим, как мы могли бы написать модульный тест для проверки нашего метода службы, используя обычные моки Mockito:

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

В этом примере нам нужно имитировать PizzaBuilder, который мы предоставляем PizzaService. Как мы видим, это нетривиальная задача, так как нам нужно вернуть макет, который будет возвращать макет для каждого вызова в нашем свободном API.

Это приводит к сложной иерархии фиктивных объектов, которую сложно понять и которую сложно поддерживать.

4. Глубокая заглушка на помощь

К счастью, Mockito предоставляет действительно удобную функцию, называемую глубокой заглушкой, которая позволяет нам указать режим ответа при создании макета.

Чтобы сделать глубокую заглушку, мы просто добавляем константу Mockito.RETURNS_DEEP_STUBS в качестве дополнительного аргумента при создании макета: насмехаться. Это позволяет смоделировать результат полной цепочки методов или, в нашем случае, свободного API за один раз.

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

Это приводит к гораздо более элегантному решению и тесту, который намного легче понять, чем тот, который мы видели в предыдущем разделе. По сути, мы избегаем необходимости создавать сложную иерархию фиктивных объектов.

Мы также можем использовать этот режим ответа напрямую с аннотацией @Mock:

Следует отметить, что проверка будет работать только с последним макетом в цепочке.

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

5. Заключение

В этом кратком руководстве мы увидели, как мы можем использовать Mockito для имитации простого API Fluent. Во-первых, мы рассмотрели традиционный метод насмешек и поняли трудности, связанные с этим методом.

Затем мы рассмотрели пример, использующий малоизвестную функцию Mockito, называемую глубокими заглушками, которая позволяет более элегантно имитировать наши плавные API.

Как всегда, полный исходный код статьи доступен на GitHub.

«