Перейти к основному содержимому

Mockito и Fluent API

· 4 мин. чтения

1. Введение

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

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

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

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

2. Простой свободный API

В этом руководстве мы будем использовать шаблон проектирования Builder, чтобы проиллюстрировать простой гибкий 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 в качестве дополнительного аргумента при создании макета:

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

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

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

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

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

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

5. Вывод

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

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

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