Мокирование void-методов в Java с Mockito: техники и примеры
Для кого эта статья:
- Разработчики на Java, особенно те, кто работает с тестированием кода
- Специалисты по качеству (QA), заинтересованные в методах тестирования
Профессионалы, изучающие или желающие улучшить свои навыки работы с Mockito и тестированием void-методов
Тестирование void-методов в Java – это настоящая головоломка для многих разработчиков. Как проверить метод, который ничего не возвращает? Как убедиться, что он выполнил необходимые действия? Mockito предоставляет элегантное решение этой проблемы, но требует особого подхода. Я поделюсь техниками мокирования void-методов, которые не только сделают ваши тесты надежными, но и значительно сократят время на их разработку. Эти приемы стали частью моего повседневного арсенала, и сегодня они станут частью вашего. 🧪
Погрузитесь глубже в мир профессионального Java-тестирования на Курсе Java-разработки от Skypro. Программа включает не только фундаментальные навыки мокирования с Mockito, но и полный стек технологий для создания надежного, хорошо протестированного кода. Узнайте, как писать тесты, которые не ломаются при каждом рефакторинге, и превратите тестирование из обязанности в конкурентное преимущество.
Особенности void методов при работе с Mockito
Void-методы представляют собой особый случай в тестировании из-за отсутствия возвращаемого значения, которое можно было бы проверить. В Mockito для работы с ними существует специфический синтаксис, отличающийся от привычного when().thenReturn().
Основное отличие в тестировании void-методов заключается в использовании семейства методов do*() вместо when(). Это связано с принципиальной особенностью Java: вызов метода без возвращаемого значения нельзя использовать как часть выражения.
Михаил Соколов, Lead Java Developer
В нашем проекте по разработке платежной системы я столкнулся с необходимостью тестирования критического компонента — сервиса уведомлений. Большинство методов этого сервиса были типа void, ведь они просто отправляли уведомления, не возвращая данных. Попытка использовать стандартный подход с
when()привела к ошибке компиляции, и мне пришлось быстро перестраиваться. После нескольких часов изучения документации Mockito я открыл для себя мирdo*()методов. Это было похоже на обнаружение потайной двери — внезапно тестирование void-методов стало не только возможным, но и элегантным.
Рассмотрим ключевые особенности void методов при тестировании с помощью Mockito:
- Синтаксическое отличие: Используются методы
doNothing(),doThrow(),doAnswer()вместо стандартныхwhen().thenReturn()илиwhen().thenThrow() - Верификация вместо ассертов: Основной способ тестирования — проверка факта вызова метода, а не его возвращаемого значения
- Строгий порядок конфигурации: Сначала указывается действие (
doNothing()), затем мок (when(mock)), и только потом — метод - Расширенные возможности взаимодействия: Можно имитировать побочные эффекты, выбрасывать исключения или выполнять произвольный код
| Метод Mockito | Применение для void методов | Эквивалент для методов с возвращаемым значением |
|---|---|---|
doNothing().when(mock).method() | Базовое мокирование без действий | when(mock.method()).thenReturn(value) |
doThrow().when(mock).method() | Имитация выбрасывания исключения | when(mock.method()).thenThrow(exception) |
doAnswer().when(mock).method() | Выполнение произвольного кода | when(mock.method()).thenAnswer(answer) |
verify(mock).method() | Проверка вызова void метода | verify(mock).method() + assertEquals(expected, actual) |
Что важно понимать: при работе с void методами акцент смещается с проверки результатов на проверку взаимодействий. Это требует иного мышления и подхода к организации тестов. 🤔

Использование doNothing() для базового мокирования void методов
Метод doNothing() — это базовый инструмент при мокировании void методов. Он указывает Mockito, что мокируемый метод не должен выполнять никаких действий при вызове. Звучит просто, но на практике открывает множество возможностей.
Синтаксис использования doNothing() выглядит следующим образом:
doNothing().when(mockObject).voidMethod(arguments);
Хотя может показаться, что doNothing() не добавляет ценности (ведь мок по умолчанию ничего не делает), его использование делает тесты более читаемыми и явно указывает на намерение тестировщика. Кроме того, в некоторых сценариях он незаменим:
- При работе с шпионами (spy): в отличие от моков, шпионы по умолчанию вызывают реальные методы
- При перегрузке поведения: когда нужно изменить ранее определенное поведение мока
- При создании цепочек действий: в комбинации с
doThrow()или другими методами
Рассмотрим практический пример использования doNothing() для тестирования сервиса уведомлений:
// Класс, который мы тестируем
public class NotificationService {
private EmailSender emailSender;
public void notifyUser(User user, String message) {
if (user == null || message == null) {
throw new IllegalArgumentException("User and message cannot be null");
}
emailSender.sendEmail(user.getEmail(), "Notification", message);
}
}
// Тест с использованием doNothing()
@Test
public void testNotifyUser() {
// Arrange
EmailSender mockEmailSender = mock(EmailSender.class);
NotificationService service = new NotificationService(mockEmailSender);
User user = new User("test@example.com");
String message = "Test notification";
doNothing().when(mockEmailSender).sendEmail(anyString(), anyString(), anyString());
// Act
service.notifyUser(user, message);
// Assert
verify(mockEmailSender).sendEmail(
eq("test@example.com"),
eq("Notification"),
eq("Test notification")
);
}
В этом примере мы:
- Создаем мок EmailSender и инжектируем его в тестируемый сервис
- Настраиваем мок с помощью
doNothing(), указывая, что метод sendEmail не должен выполнять никаких действий - Вызываем тестируемый метод
- Проверяем, что метод sendEmail был вызван с ожидаемыми аргументами
Такой подход позволяет сосредоточиться на тестировании логики NotificationService без фактической отправки электронных писем. 📧
Тестирование исключений с помощью doThrow() в Mockito
Одним из критических аспектов тестирования является проверка правильности обработки исключительных ситуаций. Метод doThrow() позволяет симулировать выбрасывание исключений из void-методов мока, что открывает возможности для тщательного тестирования обработки ошибок.
Базовый синтаксис использования doThrow() выглядит следующим образом:
doThrow(ExceptionClass).when(mockObject).voidMethod(arguments);
Этот метод особенно ценен, когда необходимо проверить, как ваш код обрабатывает ошибки внешних сервисов или компонентов, с которыми взаимодействует через void-методы.
Анна Петрова, QA Lead
В моей практике был случай, когда мы упустили тестирование сценария с исключением в сервисе логирования. Метод logOperation() был void и не вызывал подозрений, пока однажды в боевой среде не произошел сбой из-за переполнения буфера логов. Система не справилась с ошибкой и прервала критическую бизнес-операцию вместо того, чтобы продолжить работу без логирования. После этого инцидента я стала адептом
doThrow()— мы добавили тесты, имитирующие все возможные исключения из внешних зависимостей, и нашли еще несколько потенциальных проблем. Теперь это стандартная практика в нашей команде: если метод void, мы обязательно проверяем сценарии с исключениями.
Рассмотрим практический пример использования doThrow() для тестирования сервиса транзакций:
// Класс, который мы тестируем
public class TransactionService {
private PaymentGateway paymentGateway;
private TransactionLogger logger;
public void processPayment(Payment payment) throws PaymentFailedException {
try {
paymentGateway.executePayment(payment);
logger.logSuccess(payment.getId());
} catch (GatewayException e) {
logger.logFailure(payment.getId(), e.getMessage());
throw new PaymentFailedException("Payment processing failed", e);
}
}
}
// Тест с использованием doThrow()
@Test
public void testProcessPaymentWhenGatewayFails() {
// Arrange
PaymentGateway mockGateway = mock(PaymentGateway.class);
TransactionLogger mockLogger = mock(TransactionLogger.class);
TransactionService service = new TransactionService(mockGateway, mockLogger);
Payment payment = new Payment("P12345", 100.00);
doThrow(new GatewayException("Connection timeout"))
.when(mockGateway).executePayment(any(Payment.class));
// Act & Assert
try {
service.processPayment(payment);
fail("Should have thrown PaymentFailedException");
} catch (PaymentFailedException e) {
verify(mockLogger).logFailure(eq("P12345"), eq("Connection timeout"));
verify(mockLogger, never()).logSuccess(anyString());
}
}
В этом примере мы:
- Создаем моки для PaymentGateway и TransactionLogger
- Настраиваем PaymentGateway так, чтобы он выбрасывал GatewayException при вызове executePayment
- Вызываем тестируемый метод и проверяем, что он корректно преобразует GatewayException в PaymentFailedException
- Дополнительно проверяем, что метод logFailure был вызван с правильными аргументами, а logSuccess не вызывался вовсе
doThrow() позволяет создавать более комплексные сценарии тестирования, например:
// Разные исключения для разных аргументов
doThrow(InvalidDataException.class)
.when(mockValidator).validate(argThat(data -> data.getAmount() <= 0));
doThrow(AuthorizationException.class)
.when(mockValidator).validate(argThat(data -> data.getUserId() == null));
// Последовательность исключений при повторных вызовах
doThrow(TimeoutException.class, ServiceUnavailableException.class)
.when(mockService).performOperation();
Такой подход к тестированию гарантирует, что ваше приложение правильно реагирует на исключения, возникающие в void методах, что критически важно для обеспечения стабильности системы. 🛡️
Создание сложного поведения void методов через doAnswer()
Метод doAnswer() представляет собой мощный инструмент, позволяющий реализовать динамическое поведение void методов, которое выходит за рамки простого "ничего не делать" или "выбрасывать исключение". С его помощью можно выполнять произвольный код при вызове мокируемого метода.
Базовый синтаксис использования doAnswer() выглядит так:
doAnswer(invocation -> {
// Произвольный код
return null; // для void методов всегда возвращаем null
}).when(mockObject).voidMethod(arguments);
Ключевые случаи применения doAnswer() включают:
- Имитация побочных эффектов: изменение состояния других объектов или переменных
- Условная логика: различное поведение в зависимости от аргументов вызова
- Асинхронное поведение: имитация задержек или многопоточных операций
- Захват аргументов: сохранение параметров для последующего анализа
Рассмотрим практический пример использования doAnswer() для тестирования кэширующего сервиса:
// Класс, который мы тестируем
public class UserService {
private UserRepository repository;
private CacheManager cacheManager;
public void updateUserProfile(User user) {
repository.save(user);
cacheManager.invalidateCache(user.getId());
}
public User getUserById(String userId) {
// Сначала проверяем кэш
User cachedUser = cacheManager.getFromCache(userId);
if (cachedUser != null) {
return cachedUser;
}
// Если нет в кэше, получаем из репозитория и кэшируем
User user = repository.findById(userId);
if (user != null) {
cacheManager.addToCache(userId, user);
}
return user;
}
}
// Тест с использованием doAnswer()
@Test
public void testUpdateInvalidatesAndRefreshesCache() {
// Arrange
UserRepository mockRepo = mock(UserRepository.class);
CacheManager mockCache = mock(CacheManager.class);
UserService service = new UserService(mockRepo, mockCache);
User originalUser = new User("U123", "Original Name");
User updatedUser = new User("U123", "Updated Name");
// Имитация работы кэша с помощью Map
Map<String, User> fakeCache = new HashMap<>();
fakeCache.put("U123", originalUser);
// Получение из кэша
when(mockCache.getFromCache("U123")).thenAnswer(inv -> fakeCache.get("U123"));
// Имитация удаления из кэша
doAnswer(inv -> {
String userId = inv.getArgument(0);
fakeCache.remove(userId);
return null;
}).when(mockCache).invalidateCache(anyString());
// Имитация добавления в кэш
doAnswer(inv -> {
String userId = inv.getArgument(0);
User user = inv.getArgument(1);
fakeCache.put(userId, user);
return null;
}).when(mockCache).addToCache(anyString(), any(User.class));
when(mockRepo.findById("U123")).thenReturn(updatedUser);
// Act
User beforeUpdate = service.getUserById("U123");
service.updateUserProfile(updatedUser);
User afterUpdate = service.getUserById("U123");
// Assert
assertEquals("Original Name", beforeUpdate.getName());
assertEquals("Updated Name", afterUpdate.getName());
verify(mockCache).invalidateCache("U123");
}
В этом примере мы:
- Создаем фейковый кэш в виде HashMap для имитации реального кэширования
- Используем
doAnswer()для определения поведения методов invalidateCache и addToCache - Проверяем, что после обновления профиля пользователя кэш корректно инвалидируется и обновляется
doAnswer() особенно полезен при тестировании многопоточных сценариев:
@Test
public void testAsyncOperation() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean callbackExecuted = new AtomicBoolean(false);
doAnswer(inv -> {
new Thread(() -> {
try {
Thread.sleep(100); // Имитация асинхронной операции
callbackExecuted.set(true);
latch.countDown();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
return null;
}).when(mockProcessor).processAsync(any());
service.startAsyncProcessing(new Request());
latch.await(500, TimeUnit.MILLISECONDS);
assertTrue(callbackExecuted.get());
}
Важно помнить о некоторых особенностях при использовании doAnswer():
- Лямбда-выражение должно всегда возвращать null для void методов
- Аргументы вызова доступны через invocation.getArgument(index)
- Типы аргументов нужно приводить к нужным, так как они возвращаются как Object
- Исключения, выброшенные внутри Answer, пробрасываются вызывающему коду
doAnswer() предоставляет максимальную гибкость при мокировании void методов, позволяя создавать сложные сценарии тестирования и имитировать практически любое поведение. 🧩
Верификация вызовов void методов с помощью verify()
Поскольку void-методы не возвращают результат, который можно проверить, основным способом их тестирования становится верификация факта и параметров вызова. Метод verify() в Mockito предоставляет мощный инструмент для такой проверки, позволяя убедиться, что метод был вызван с правильными аргументами и нужное количество раз.
Базовый синтаксис верификации выглядит следующим образом:
verify(mockObject, times(n)).voidMethod(arguments);
Метод verify() можно использовать с различными модификаторами для уточнения ожидаемого поведения:
- times(n): проверка точного количества вызовов (по умолчанию times(1))
- never(): проверка, что метод не вызывался
- atLeastOnce(): проверка, что метод вызывался хотя бы один раз
- atLeast(n): проверка, что метод вызывался не менее n раз
- atMost(n): проверка, что метод вызывался не более n раз
Рассмотрим практический пример верификации вызовов в системе аудита:
// Класс, который мы тестируем
public class OrderProcessor {
private InventoryService inventoryService;
private AuditLogger auditLogger;
public void processOrder(Order order) {
if (order == null || order.getItems().isEmpty()) {
auditLogger.logWarning("Attempted to process invalid order");
return;
}
for (OrderItem item : order.getItems()) {
try {
inventoryService.reserveItem(item.getProductId(), item.getQuantity());
auditLogger.logInfo("Reserved item: " + item.getProductId());
} catch (OutOfStockException e) {
auditLogger.logError("Failed to reserve item: " + item.getProductId(), e);
// Отменяем уже зарезервированные позиции
rollbackReservations(order, item);
throw new OrderProcessingException("Cannot complete order due to inventory issues", e);
}
}
auditLogger.logInfo("Order processed successfully: " + order.getId());
}
private void rollbackReservations(Order order, OrderItem failedItem) {
for (OrderItem item : order.getItems()) {
if (item.equals(failedItem)) {
break;
}
inventoryService.cancelReservation(item.getProductId(), item.getQuantity());
auditLogger.logInfo("Cancelled reservation for: " + item.getProductId());
}
}
}
// Тест с использованием verify()
@Test
public void testOrderProcessingSuccess() {
// Arrange
InventoryService mockInventory = mock(InventoryService.class);
AuditLogger mockLogger = mock(AuditLogger.class);
OrderProcessor processor = new OrderProcessor(mockInventory, mockLogger);
Order order = new Order("ORD123");
order.addItem(new OrderItem("PROD1", 2));
order.addItem(new OrderItem("PROD2", 1));
// Act
processor.processOrder(order);
// Assert
verify(mockInventory).reserveItem("PROD1", 2);
verify(mockInventory).reserveItem("PROD2", 1);
verify(mockLogger, times(2)).logInfo(contains("Reserved item"));
verify(mockLogger).logInfo(contains("Order processed successfully"));
verify(mockLogger, never()).logError(anyString(), any(Exception.class));
verify(mockLogger, never()).logWarning(anyString());
}
@Test
public void testOrderProcessingOutOfStock() {
// Arrange
InventoryService mockInventory = mock(InventoryService.class);
AuditLogger mockLogger = mock(AuditLogger.class);
OrderProcessor processor = new OrderProcessor(mockInventory, mockLogger);
Order order = new Order("ORD123");
order.addItem(new OrderItem("PROD1", 2));
order.addItem(new OrderItem("PROD2", 1));
doThrow(new OutOfStockException("Out of stock"))
.when(mockInventory).reserveItem("PROD2", 1);
// Act & Assert
try {
processor.processOrder(order);
fail("Should throw OrderProcessingException");
} catch (OrderProcessingException e) {
verify(mockInventory).reserveItem("PROD1", 2);
verify(mockInventory).reserveItem("PROD2", 1);
verify(mockInventory).cancelReservation("PROD1", 2);
verify(mockLogger).logInfo(contains("Reserved item: PROD1"));
verify(mockLogger).logError(contains("Failed to reserve item: PROD2"), any(OutOfStockException.class));
verify(mockLogger).logInfo(contains("Cancelled reservation for: PROD1"));
verify(mockLogger, never()).logInfo(contains("Order processed successfully"));
}
}
При верификации вызовов void методов особенно полезны матчеры аргументов:
- any(), anyInt(), anyString(): любое значение соответствующего типа
- eq(): точное соответствие значению
- contains(), startsWith(), endsWith(): проверки для строк
- argThat(): пользовательские проверки с помощью предиката
- isNull(), isNotNull(): проверка на null
Для более сложных сценариев Mockito позволяет верифицировать порядок вызовов:
// Проверка порядка вызовов
InOrder inOrder = inOrder(mockInventory, mockLogger);
inOrder.verify(mockInventory).reserveItem("PROD1", 2);
inOrder.verify(mockLogger).logInfo(contains("Reserved item: PROD1"));
inOrder.verify(mockInventory).reserveItem("PROD2", 1);
Также можно захватывать аргументы для дополнительной проверки:
ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class);
verify(mockLogger, atLeastOnce()).logInfo(messageCaptor.capture());
List<String> capturedMessages = messageCaptor.getAllValues();
assertTrue(capturedMessages.stream().anyMatch(msg -> msg.contains("Order processed")));
Верификация с помощью verify() — это краеугольный камень тестирования void методов, позволяющий убедиться, что они вызывались правильно и имели ожидаемые побочные эффекты. 🔍
Овладение техниками мокирования void-методов превращает их из сложных точек для тестирования в надежно покрытые участки кода. Используйте
doNothing()для базовой имитации,doThrow()для тестирования исключительных ситуаций,doAnswer()для сложной логики иverify()для подтверждения корректности взаимодействий. Эти инструменты вместе формируют полноценную стратегию тестирования методов без возвращаемого значения, позволяя создавать надежные, понятные и поддерживаемые тесты даже для самых сложных частей вашего приложения. Помните: качество кода определяется не только его функциональностью, но и тестовым покрытием — а с правильным подходом к мокированию void-методов вы можете достичь и того, и другого.