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

Обзор Mockito MockSettings

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

1. Обзор

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

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

В предыдущем уроке мы узнали, как работать с мягкими моками . В этом кратком руководстве мы узнаем, как использовать некоторые другие полезные функции, предоставляемые интерфейсом MockSettings .

2. Имитация настроек

Проще говоря, интерфейс MockSettings предоставляет Fluent API , который позволяет нам легко добавлять и комбинировать дополнительные параметры макета во время создания макета.

Когда мы создаем макет объекта, все наши макеты имеют набор настроек по умолчанию. Давайте рассмотрим простой фиктивный пример:

List mockedList = mock(List.class);

За кулисами метод макета Mockito делегирует другому перегруженному методу с набором настроек по умолчанию для нашего макета:

public static <T> T mock(Class<T> classToMock) {
return mock(classToMock, withSettings());
}

Давайте посмотрим на наши настройки по умолчанию:

public static MockSettings withSettings() {
return new MockSettingsImpl().defaultAnswer(RETURNS_DEFAULTS);
}

Как мы видим, наш стандартный набор настроек для наших фиктивных объектов очень прост. Мы настраиваем ответ по умолчанию для наших фиктивных взаимодействий. Как правило, использование RETURN_DEFAULTS возвращает какое-то пустое значение.

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

В следующих разделах мы увидим несколько примеров, когда это может пригодиться.

3. Предоставление другого ответа по умолчанию

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

Давайте представим, что у нас есть очень простая установка для макета:

PizzaService service = mock(PizzaService.class);
Pizza pizza = service.orderHouseSpecial();
PizzaSize size = pizza.getSize();

Когда мы запустим этот код, как и ожидалось, мы получим NullPointerException , потому что наш незаглушенный метод orderHouseSpecial возвращает null .

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

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

PizzaService pizzaService = mock(PizzaService.class, withSettings().defaultAnswer(RETURNS_SMART_NULLS));

Используя RETURNS_SMART_NULLS в качестве нашего ответа по умолчанию, Mockito дает нам гораздо более значимое сообщение об ошибке, которое показывает нам, где именно произошла неправильная заглушка:

org.mockito.exceptions.verification.SmartNullPointerException: 
You have a NullPointerException here:
-> at com.foreach.mockito.mocksettings.MockSettingsUnitTest.whenServiceMockedWithSmartNulls_thenExceptionHasExtraInfo(MockSettingsUnitTest.java:45)
because this method call was *not* stubbed correctly:
-> at com.foreach.mockito.mocksettings.MockSettingsUnitTest.whenServiceMockedWithSmartNulls_thenExceptionHasExtraInfo(MockSettingsUnitTest.java:44)
pizzaService.orderHouseSpecial();

Это действительно может сэкономить нам время при отладке нашего тестового кода. Перечисление « Ответы » также предоставляет некоторые другие предварительно настроенные фиктивные ответы:

  • RETURNS_DEEP_STUBS — ответ, который возвращает глубокие заглушкиэто может быть полезно при работе с Fluent API.
  • RETURNS_MOCKS — при использовании этого ответа возвращаются обычные значения, такие как пустые коллекции или пустые строки, после чего он пытается вернуть макеты .
  • CALLS_REAL_METHODS — как следует из названия, когда мы используем эту реализацию, незаглушенные методы будут делегированы реальной реализации.

4. Именование макетов и подробное ведение журнала

Мы можем дать нашему макету имя, используя метод имени MockSettings . Это может быть особенно полезно для отладки, поскольку имя, которое мы предоставляем, используется во всех ошибках проверки:

PizzaService service = mock(PizzaService.class, withSettings()
.name("pizzaServiceMock")
.verboseLogging()
.defaultAnswer(RETURNS_SMART_NULLS));

В этом примере мы объединяем эту функцию именования с подробным ведением журнала с помощью метода verboseLogging() .

Использование этого метода позволяет вести журнал в реальном времени в стандартном потоке вывода для вызовов методов на этом макете . Точно так же его можно использовать во время отладки тестов, чтобы найти неправильное взаимодействие с макетом.

Когда мы запустим наш тест, мы увидим вывод в консоли:

pizzaServiceMock.orderHouseSpecial();
invoked: -> at com.foreach.mockito.mocksettings.MockSettingsUnitTest.whenServiceMockedWithNameAndVerboseLogging_thenLogsMethodInvocations(MockSettingsUnitTest.java:36)
has returned: "Mock for Pizza, hashCode: 366803687" (com.foreach.mockito.fluentapi.Pizza$MockitoMock$168951489)

Интересно отметить, что если мы используем аннотацию @Mock , наши макеты автоматически принимают имя поля в качестве имени макета.

5. Насмешка над дополнительными интерфейсами

Иногда мы можем захотеть указать дополнительные интерфейсы, которые должен реализовать наш макет. Опять же, это может быть полезно при работе с устаревшим кодом, который мы не можем реорганизовать.

Представим, что у нас есть специальный интерфейс:

public interface SpecialInterface {
// Public methods
}

И класс, который использует этот интерфейс:

public class SimpleService {

public SimpleService(SpecialInterface special) {
Runnable runnable = (Runnable) special;
runnable.run();
}
// More service methods
}

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

SpecialInterface specialMock = mock(SpecialInterface.class);
SimpleService service = new SimpleService(specialMock);

Когда мы запустим этот код, мы получим ClassCastException . Чтобы исправить это, мы можем создать макет с несколькими типами, используя метод extraInterfaces :

SpecialInterface specialMock = mock(SpecialInterface.class, withSettings()
.extraInterfaces(Runnable.class));

Теперь наш фиктивный код создания не подведет, но мы действительно должны подчеркнуть, что приведение к необъявленному типу — это не круто.

6. Предоставление аргументов конструктора

В этом последнем примере мы увидим, как мы можем использовать MockSettings для вызова реального конструктора со значением аргумента:

@Test
public void whenMockSetupWithConstructor_thenConstructorIsInvoked() {
AbstractCoffee coffeeSpy = mock(AbstractCoffee.class, withSettings()
.useConstructor("espresso")
.defaultAnswer(CALLS_REAL_METHODS));

assertEquals("Coffee name: ", "espresso", coffeeSpy.getName());
}

На этот раз Mockito пытается использовать конструктор со значением String при создании экземпляра нашего макета AbstractCoffee . Мы также настраиваем ответ по умолчанию для делегирования реальной реализации.

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

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

В этом кратком руководстве мы увидели, как создавать макеты с дополнительными настройками макетов.

Тем не менее, мы должны повторить, что хотя иногда это полезно и, вероятно, неизбежно, в большинстве случаев мы должны стремиться писать простые тесты, используя простые макеты.

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