W3docs

Мокирование в Java с Mockito

Замена зависимостей в тестах Java с помощью Mockito: mock, when/thenReturn, verify и захват аргументов.

Модульный тест должен проверять один класс в изоляции. Но реальные классы опираются на вспомогательные объекты — базу данных, платёжный шлюз, почтовый сервис — которые работают медленно, ненадёжно или имеют побочные эффекты, нежелательные в тесте. Mockito — наиболее широко используемая Java-библиотека для замены таких объектов моками: подставными объектами, которые вы программируете возвращать заготовленные ответы, а затем проверяете, как они были вызваны. В этой главе рассматривается API Mockito, которым вы будете пользоваться каждый день, а идея подтверждается программой на чистом JDK, которую можно запустить прямо здесь.

В этой главе предполагается, что вы уже знакомы с основами тестирования, рассмотренными в Введении в JUnit 5 и Утверждениях JUnit. Mockito дополняет JUnit: JUnit запускает тест и проверяет значения, а Mockito предоставляет фиктивные вспомогательные объекты.

Зачем вообще мокировать

Тестируемый класс (system under test, или SUT) обычно получает свои зависимости через конструктор — именно это даёт внедрение зависимостей. В тесте вы передаёте ему фиктивную зависимость вместо настоящей. Хороший фиктивный объект выполняет две задачи:

  • Заглушка (Stubbing) — возвращает то значение, которое нужно тестовому сценарию (charge(...) возвращает true или выбрасывает исключение), позволяя направить SUT по конкретному пути без реального сетевого вызова.
  • Проверка (Verification) — записывает каждый полученный вызов, чтобы впоследствии тест мог убедиться, что SUT вызвал зависимость правильным образом, нужное количество раз и с нужными аргументами.

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

Создание моков и заглушка возвращаемых значений

Mockito.mock(Type.class) создаёт мок. По умолчанию каждый метод возвращает «безопасное» пустое значение — null для объектов, false для boolean, 0 для чисел. Затем вы переопределяете нужные методы с помощью when(...).thenReturn(...).

import static org.mockito.Mockito.*;

PaymentGateway gateway = mock(PaymentGateway.class);

// Stub: when charge is called with these args, return true.
when(gateway.charge("acct-7", 1999)).thenReturn(true);

// Stub a method to throw, to test error handling.
when(gateway.charge("acct-x", 1)).thenThrow(new GatewayException("down"));

Для методов void порядок меняется: doThrow(...).when(mock).method(). Заглушки также можно обобщить с помощью сопоставителей аргументов, таких как anyString() и anyInt(), чтобы они срабатывали для любого вызова, а не только для одного конкретного набора аргументов.

Проверка взаимодействий

После выполнения SUT метод verify(...) проверяет, как использовался мок. Так вы тестируете побочные эффекты — письмо, которое должно было быть отправлено, строка, которая должна была быть сохранена — не обращаясь к реальной системе.

verify(gateway).charge("acct-7", 1999);        // called exactly once (default)
verify(gateway, times(2)).charge(anyString(), anyInt());
verify(gateway, never()).refund(anyString());  // must NOT have been called
verifyNoMoreInteractions(gateway);             // nothing else happened

Наиболее распространённые режимы проверки:

РежимЗначение
times(n)Вызван ровно n раз
never()То же, что times(0)
atLeastOnce() / atLeast(n)Вызван хотя бы один раз / n раз
atMost(n)Вызван не более n раз
only()Это был единственный метод, вызванный на моке

Захват аргументов

Когда нужно проверить что именно было передано — а не просто факт вызова — используйте ArgumentCaptor. Он перехватывает фактический аргумент, чтобы можно было проверить его поля; это незаменимо, когда SUT строит объект перед тем, как передать его дальше.

ArgumentCaptor<Order> captor = ArgumentCaptor.forClass(Order.class);
verify(repository).save(captor.capture());

Order saved = captor.getValue();
assertEquals("acct-7", saved.account());
assertEquals(1999, saved.amountCents());

@Mock, @InjectMocks и шпионы

В реальных тестовых классах вы редко вызываете mock() вручную. Аннотации всё настраивают: @Mock объявляет поле-мок, @InjectMocks создаёт SUT и внедряет моки в его конструктор, а @ExtendWith(MockitoExtension.class) (JUnit 5) активирует обработку.

@ExtendWith(MockitoExtension.class)
class CheckoutServiceTest {
  @Mock PaymentGateway gateway;
  @InjectMocks CheckoutService service;   // gets the mock injected

  @Test
  void paysWhenGatewayApproves() {
    when(gateway.charge("acct-7", 1999)).thenReturn(true);
    assertEquals("PAID", service.checkout("acct-7", 1999));
    verify(gateway).charge("acct-7", 1999);
  }
}

Шпион (spy(realObject)) — промежуточный вариант: он оборачивает реальный объект и выполняет реальные методы, если только вы не заглушаете их — удобно для частичного мокирования унаследованного кода.

Внимание
Mockito может мокировать только то, что поддаётся переопределению. По умолчанию он не может мокировать final-классы, final-методы, static-методы или private-методы. Если необходимо замокировать final-класс, включите MockMaker mockito-inline; в противном случае выполните рефакторинг с использованием интерфейса.

Когда не стоит мокировать

Моки — мощный инструмент, но избыточное мокирование приводит к тестам, которые проходят, пока реальный код сломан. Используйте мок только тогда, когда реальная зависимость медленная, недетерминированная, имеет побочные эффекты или ещё не создана. Не мокируйте объекты-значения, сам тестируемый класс или чужие типы (оберните сторонний API в собственный интерфейс и мокируйте его). Если зависимость дёшева и чиста — простой калькулятор, список в памяти — используйте настоящую и проверяйте её результат напрямую.

Проработанный пример: мок, написанный вручную

Mockito недоступен в classpath этой страницы, поэтому запускаемая ниже программа строит мок вручную — небольшой класс, реализующий интерфейс зависимости, который хранит заглушённое возвращаемое значение и записывает каждый вызов. Именно такой механизм Mockito генерирует за вас во время выполнения, поэтому чтение этого кода даёт точное понимание того, что делают when/thenReturn и verify внутри.

java— editable, runs on the server

Что следует вынести из запуска:

  • Поле stubbedResult = true у MockGateway — это написанная вручную форма when(gateway.charge(...)).thenReturn(true); поскольку заглушка вернула true, SUT вывел result : PAID без единого реального платежа.
  • invocationCount == 1, возвращающее true, — это именно то, что проверяет verify(gateway).charge(...); мок подсчитал, что был вызван один раз, и это превращает вопрос «произошло ли это взаимодействие?» в утверждение «прошёл/не прошёл».
  • Список calls зафиксировал charge(acct-7, 1999) — идея захвата аргументов, лежащая в основе ArgumentCaptor: мок запоминает не только факт вызова, но и с какими аргументами, чтобы тест мог проверить переданные значения.
  • Воссоздание мока с stubbedResult = false направило SUT по другой ветви и вывело declined result : DECLINED, показывая, как один фиктивный объект позволяет воспроизвести любой сценарий, который может породить реальная зависимость.
  • Защитная проверка вернула INVALID до обращения к шлюзу, поэтому invocationCount == 0 вывело true — запускаемое доказательство verify(gateway, never()).charge(...), подтверждающее, что зависимость намеренно не была затронута.

Практика

Практика
В модульном тесте на основе Mockito, что делает вызов verify(gateway, never()).charge(anyString(), anyInt())?
В модульном тесте на основе Mockito, что делает вызов verify(gateway, never()).charge(anyString(), anyInt())?
Was this page helpful?