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 .