Мокирование в 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)) — промежуточный вариант: он оборачивает реальный объект и выполняет реальные методы, если только вы не заглушаете их — удобно для частичного мокирования унаследованного кода.
final-классы, final-методы, static-методы или private-методы. Если необходимо замокировать final-класс, включите MockMaker mockito-inline; в противном случае выполните рефакторинг с использованием интерфейса.Когда не стоит мокировать
Моки — мощный инструмент, но избыточное мокирование приводит к тестам, которые проходят, пока реальный код сломан. Используйте мок только тогда, когда реальная зависимость медленная, недетерминированная, имеет побочные эффекты или ещё не создана. Не мокируйте объекты-значения, сам тестируемый класс или чужие типы (оберните сторонний API в собственный интерфейс и мокируйте его). Если зависимость дёшева и чиста — простой калькулятор, список в памяти — используйте настоящую и проверяйте её результат напрямую.
Проработанный пример: мок, написанный вручную
Mockito недоступен в classpath этой страницы, поэтому запускаемая ниже программа строит мок вручную — небольшой класс, реализующий интерфейс зависимости, который хранит заглушённое возвращаемое значение и записывает каждый вызов. Именно такой механизм Mockito генерирует за вас во время выполнения, поэтому чтение этого кода даёт точное понимание того, что делают when/thenReturn и verify внутри.
Что следует вынести из запуска:
- Поле
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(...), подтверждающее, что зависимость намеренно не была затронута.