«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.
«