Тестирование void-методов в Mockito: проверка исключений с doThrow
Для кого эта статья:
- Java-разработчики с опытом работы с юнит-тестами
- Специалисты по тестированию программного обеспечения
Учащиеся и практикующие программисты, желающие улучшить навыки тестирования методов в Java
Тестирование void-методов часто становится камнем преткновения для Java-разработчиков при написании юнит-тестов. Особую сложность представляет проверка корректности выброса исключений — как тестировать то, что не возвращает значение? Библиотека Mockito предлагает элегантное решение этой проблемы через механизм
doThrow().when(), позволяющий имитировать исключения в void-методах с хирургической точностью. Мастерство работы с этим инструментом отличает опытного тестировщика от новичка, особенно когда речь идёт о сложных сценариях взаимодействия компонентов. 🧪
Если вы часто сталкиваетесь с трудностями при тестировании void-методов и хотите поднять свои навыки написания юнит-тестов на профессиональный уровень, обратите внимание на Курс Java-разработки от Skypro. Программа включает глубокое погружение в тестирование с Mockito, где вы научитесь профессионально обрабатывать исключения, создавать надёжные тесты и применять best practices в реальных проектах под руководством экспертов-практиков.
Особенности работы с void-методами в Mockito
При тестировании void-методов в Mockito возникает фундаментальное отличие от тестирования методов, возвращающих значение. Стандартный подход when().thenReturn() или when().thenThrow() не может быть применён напрямую, поскольку вызов void-метода не даёт объекта, к которому можно применить метод when().
Эта особенность связана с тем, как Mockito перехватывает и обрабатывает вызовы методов. Для методов с возвращаемым значением инструмент может создать промежуточный прокси-объект, на котором настраиваются ожидания. Для void-методов такая схема технически неосуществима.
Андрей Савин, технический лид команды тестирования
Однажды наша команда столкнулась с необходимостью тестирования сервиса обработки платежей. Ключевой void-метод
processTransaction()должен был выбрасыватьInsufficientFundsExceptionпри нехватке средств. Первоначально мы пытались использовать стандартный паттернwhen().thenThrow(), но тесты не компилировались. Мы потратили почти день на отладку, пока не разобрались в особенностях работы Mockito с void-методами. Переход наdoThrow().when()решил проблему, а наш тимлид внёс это знание в корпоративную базу знаний как обязательный паттерн.
Для работы с void-методами Mockito предлагает альтернативный синтаксис, основанный на "do*" семействе методов:
doThrow()— для имитации выброса исключенияdoAnswer()— для выполнения произвольной логикиdoNothing()— для явного указания, что метод не должен ничего делатьdoCallRealMethod()— для вызова реальной реализации метода
Эти методы позволяют сначала определить поведение, а затем указать, к какому методу оно должно быть применено. Такой подход обратен традиционному, но предоставляет необходимую гибкость для тестирования void-методов. 🔄
| Тип метода | Стандартный синтаксис | Альтернативный синтаксис | Применимость для void-методов |
|---|---|---|---|
| Возвращающий значение | when(mock.method()).thenReturn(value) | doReturn(value).when(mock).method() | Не применимо |
| Выбрасывающий исключение | when(mock.method()).thenThrow(exception) | doThrow(exception).when(mock).method() | Только альтернативный синтаксис |
| Void-метод | Не применимо | doNothing().when(mock).method() | Только альтернативный синтаксис |
Важно понимать, что тестирование void-методов часто требует проверки не результата, а побочных эффектов или взаимодействий с другими компонентами. В случае с исключениями мы проверяем, что метод действительно выбрасывает ожидаемое исключение при определённых условиях.

Механизм doThrow().when() для имитации исключений
Механизм doThrow().when() представляет собой мощный инструмент Mockito, специально разработанный для имитации исключений в void-методах. Его работа основана на двухэтапном процессе: сначала мы определяем, какое исключение должно быть выброшено, а затем указываем, при вызове какого метода это должно произойти.
Базовый синтаксис выглядит следующим образом:
// Настройка мока для выброса исключения
doThrow(ExceptionType.class).when(mockObject).voidMethod(arguments);
// Тестирование выброса исключения
assertThrows(ExceptionType.class, () -> {
mockObject.voidMethod(arguments);
});
Этот подход позволяет гибко настраивать поведение мокированных объектов в различных сценариях тестирования. Рассмотрим ключевые возможности этого механизма: 🛠️
- Выброс конкретных типов исключений — позволяет имитировать как проверяемые (checked), так и непроверяемые (unchecked) исключения
- Выброс исключений с конкретными параметрами — возможность настроить детали выбрасываемого исключения
- Условный выброс исключений — настройка выброса исключения только при определённых аргументах метода
- Последовательный выброс разных исключений — имитация различного поведения при последовательных вызовах
Рассмотрим практические примеры применения этих возможностей:
// Выброс конкретного исключения
doThrow(new IOException("Network failure")).when(fileProcessor).processFile("/path/to/file");
// Последовательный выброс разных исключений
doThrow(IOException.class, IllegalStateException.class)
.when(fileProcessor).processFile(anyString());
// Условный выброс исключения в зависимости от аргумента
doThrow(IllegalArgumentException.class)
.when(validator).validateData(argThat(data -> data == null || data.isEmpty()));
Михаил Петров, руководитель отдела обеспечения качества
Работая над системой финансовой отчётности, мы столкнулись с проблемой тестирования компонента для отправки критически важных уведомлений. Метод
sendAlertNotification()не возвращал результатов, но должен был выбрасыватьNotificationFailedExceptionпри проблемах с сервисом оповещений.Сложность заключалась в том, что исключение должно было содержать специфический код ошибки в зависимости от причины сбоя. Используя
doThrow().when(), мы смогли создать детальные тест-кейсы для каждого сценария ошибки:JavaСкопировать код// Тест для сценария недоступности сервиса NotificationFailedException serviceUnavailable = new NotificationFailedException("Service unavailable", 503); doThrow(serviceUnavailable).when(notificationService) .sendAlertNotification(eq(CRITICAL), anyString()); // Проверка выброса исключения с правильным кодом NotificationFailedException exception = assertThrows( NotificationFailedException.class, () -> notificationService.sendAlertNotification(CRITICAL, "System down") ); assertEquals(503, exception.getErrorCode());Этот подход позволил нам обеспечить 100% тестовое покрытие всех сценариев ошибок и существенно повысить надёжность системы.
Важно отметить, что doThrow() может быть использован и для методов, возвращающих значения, однако в этом случае классический синтаксис when().thenThrow() обычно более читаем и предпочтителен. Для void-методов doThrow().when() — единственно возможный подход.
| Функциональность | Синтаксис | Особенности применения |
|---|---|---|
| Выброс одиночного исключения | doThrow(ExceptionClass).when(mock).method() | Наиболее распространённый сценарий |
| Выброс исключения с сообщением | doThrow(new ExceptionClass("message")).when(mock).method() | Позволяет проверить сообщение исключения |
| Последовательные исключения | doThrow(FirstException.class, SecondException.class).when(mock).method() | Разные исключения при последовательных вызовах |
| Условный выброс исключения | doThrow(Exception.class).when(mock).method(argThat(predicate)) | Исключение выбрасывается только при соответствии аргумента условию |
Практические случаи проверки исключений в void-методах
Тестирование выброса исключений в void-методах играет ключевую роль в обеспечении надёжности программных систем. Рассмотрим несколько практических случаев, в которых такое тестирование особенно важно, и подходы к их реализации с использованием Mockito.
Сценарий 1: Проверка бизнес-правил и валидации
Многие методы валидации не возвращают результатов, а выбрасывают исключения при нарушении бизнес-правил. Такой подход является идиоматическим для Java и широко применяется в enterprise-системах.
@Test
void shouldThrowValidationExceptionWhenUserDataInvalid() {
// Arrange
User invalidUser = new User(null, "");
doThrow(ValidationException.class).when(userValidator).validate(invalidUser);
// Act & Assert
assertThrows(ValidationException.class, () -> {
userValidator.validate(invalidUser);
});
// Verify
verify(userValidator).validate(invalidUser);
}
Сценарий 2: Операции с внешними системами
Методы, выполняющие операции с файловой системой, сетью или базами данных, часто реализованы как void-методы, которые могут выбрасывать различные исключения при сбоях.
@Test
void shouldThrowIOExceptionWhenFileSystemUnavailable() {
// Arrange
String filePath = "/path/to/critical/file.dat";
doThrow(IOException.class).when(fileService).writeData(eq(filePath), any(byte[].class));
// Act & Assert
byte[] data = "Important data".getBytes();
assertThrows(IOException.class, () -> {
fileService.writeData(filePath, data);
});
}
Сценарий 3: Каскадный выброс исключений через несколько слоёв
Особую ценность тестирование void-методов представляет при проверке корректного преобразования и прокидывания исключений между слоями приложения. 🔄
@Test
void shouldWrapDatabaseExceptionInServiceException() {
// Arrange
Order order = new Order(/*...*/);
doThrow(DatabaseException.class).when(orderRepository).save(order);
// Act & Assert
ServiceException exception = assertThrows(ServiceException.class, () -> {
orderService.processOrder(order);
});
// Проверяем, что исключение сервисного уровня содержит оригинальное исключение
assertTrue(exception.getCause() instanceof DatabaseException);
}
Сценарий 4: Проверка поведения при многократном выбросе исключений
Иногда необходимо проверить, как система реагирует на последовательные сбои, например, при реализации механизмов повторных попыток.
@Test
void shouldRetryThreeTimesBeforeGivingUp() {
// Arrange – настраиваем выброс исключения при каждом вызове
doThrow(TransientNetworkException.class).when(networkClient).sendData(any());
// Act & Assert
assertThrows(MaxRetriesExceededException.class, () -> {
retryableService.sendWithRetry("payload");
});
// Verify – проверяем, что было сделано ровно 3 попытки
verify(networkClient, times(3)).sendData(any());
}
Сценарий 5: Проверка очередности действий при выбросе исключения
Важно не только проверить сам факт выброса исключения, но и убедиться, что система корректно выполняет сопутствующие действия, такие как откат транзакции или освобождение ресурсов.
@Test
void shouldReleaseResourcesWhenExceptionOccurs() {
// Arrange
Resource resource = mock(Resource.class);
doThrow(ProcessingException.class).when(processor).process(any());
// Act
try {
resourceService.processWithResource(resource);
fail("Expected exception was not thrown");
} catch (ProcessingException e) {
// Expected exception
}
// Verify – проверяем, что ресурс был освобожден несмотря на исключение
verify(resource).acquire();
verify(resource).release();
}
При тестировании void-методов, выбрасывающих исключения, важно учитывать следующие аспекты:
- Тип исключения — проверяйте не только факт выброса исключения, но и его конкретный тип
- Сообщение исключения — убедитесь, что сообщение содержит полезную диагностическую информацию
- Причину исключения — для обёрнутых исключений проверяйте корректность цепочки причин
- Побочные эффекты — проверяйте, что все необходимые действия (логирование, освобождение ресурсов) выполняются даже при выбросе исключения
Такой комплексный подход к тестированию исключений в void-методах существенно повышает надёжность и отказоустойчивость приложения в целом.
Сравнение when().thenThrow() и doThrow().when()
Библиотека Mockito предлагает два основных подхода к имитации выброса исключений: классический when().thenThrow() и альтернативный doThrow().when(). Понимание различий между ними критически важно для эффективного тестирования, особенно когда речь идёт о void-методах. ⚖️
Основное различие связано с механизмом работы Mockito при настройке моков. Когда вы используете when(mock.method()), Mockito фактически вызывает указанный метод, чтобы перехватить его и настроить поведение. Для методов, возвращающих значение, это не проблема, но для void-методов такой вызов невозможно перехватить стандартным способом.
| Характеристика | when().thenThrow() | doThrow().when() |
|---|---|---|
| Работа с void-методами | Не поддерживается (ошибка компиляции) | Полностью поддерживается |
| Синтаксическая читаемость | Более естественная для многих разработчиков | Менее привычная, "обратный" порядок |
| Фактический вызов метода при настройке | Да (метод вызывается в when()) | Нет (метод не вызывается при настройке) |
| Поддержка шпионов (spy) | Ограниченная (может вызвать реальный метод) | Полная (предотвращает вызов реального метода) |
| Применимость к методам с побочными эффектами | Проблематично (побочные эффекты срабатывают) | Безопасно (побочные эффекты не срабатывают) |
Рассмотрим некоторые ключевые сценарии, где разница между подходами становится особенно заметной:
Сценарий 1: Тестирование void-методов
// Не компилируется – void-метод не может использоваться в when()
when(emailService.send(anyString(), anyString())).thenThrow(MailServerException.class);
// Корректный подход для void-методов
doThrow(MailServerException.class).when(emailService).send(anyString(), anyString());
Сценарий 2: Работа со шпионами (spy)
При использовании шпионов (spy) в Mockito, которые вызывают реальные методы, когда не настроено иное поведение, when() может привести к нежелательным побочным эффектам:
// Создаем шпиона
FileLogger loggerSpy = spy(new FileLogger());
// Опасно – реальный метод writeLog будет вызван во время настройки!
when(loggerSpy.writeLog("error")).thenThrow(IOException.class);
// Безопасно – реальный метод не будет вызван при настройке
doThrow(IOException.class).when(loggerSpy).writeLog("error");
Сценарий 3: Последовательные исключения
Оба подхода позволяют настраивать последовательные исключения, но синтаксис немного различается:
// Для методов с возвращаемым значением
when(calculator.divide(10, 0))
.thenThrow(ArithmeticException.class)
.thenThrow(IllegalStateException.class);
// Для void-методов
doThrow(FileNotFoundException.class, IOException.class)
.when(fileProcessor).processFile(anyString());
Выбор между when().thenThrow() и doThrow().when() должен основываться на следующих принципах:
- Для void-методов — используйте исключительно
doThrow().when(), так как альтернатива невозможна - Для методов с возвращаемым значением — предпочтительно
when().thenThrow()из-за более естественного синтаксиса - При работе со шпионами — используйте
doThrow().when()для избежания нежелательных вызовов реальных методов - При тестировании методов с побочными эффектами —
doThrow().when()будет безопаснее
Для обеспечения единообразия кодовой базы некоторые команды выбирают единый стиль (обычно doThrow().when()) для всех типов методов, жертвуя немного читаемостью в пользу последовательности.
Стоит отметить, что аналогичные различия существуют и для других методов настройки поведения в Mockito: when().thenReturn() vs doReturn().when(), when().thenAnswer() vs doAnswer().when() и т.д. Понимание этих различий является ключом к эффективному использованию Mockito в тестировании.
Лучшие практики тестирования исключений в Mockito
Тестирование исключений в void-методах с использованием Mockito требует особого внимания к деталям и следования определенным практикам. Применение этих практик обеспечивает не только корректность тестов, но и их читаемость, поддерживаемость и информативность при сбоях. 🏆
1. Используйте assertThrows вместо try-catch
Современные фреймворки тестирования, такие как JUnit 5, предлагают специальные утверждения для проверки исключений, которые делают тесты более читаемыми:
// Предпочтительный способ
Exception exception = assertThrows(ServiceException.class, () -> {
service.performOperation();
});
assertEquals("Expected error message", exception.getMessage());
// Устаревший подход с try-catch
try {
service.performOperation();
fail("Expected ServiceException was not thrown");
} catch (ServiceException e) {
assertEquals("Expected error message", e.getMessage());
}
2. Проверяйте не только тип, но и содержимое исключения
Полноценная проверка исключения должна включать в себя проверку:
- Типа исключения и его подтипа
- Текста сообщения (полностью или по ключевым фрагментам)
- Кода ошибки (если применимо)
- Вложенных причин исключения
- Дополнительных атрибутов специфичных для вашего исключения
Exception exception = assertThrows(ValidationException.class, () -> {
validator.validateUser(invalidUser);
});
// Проверка сообщения
assertTrue(exception.getMessage().contains("username cannot be empty"));
// Проверка кода ошибки (если ваше исключение содержит такой атрибут)
assertEquals(ErrorCode.INVALID_INPUT, ((ValidationException) exception).getErrorCode());
// Проверка причины
assertTrue(exception.getCause() instanceof IllegalArgumentException);
3. Используйте матчеры аргументов для более точной настройки условий выброса исключения
Mockito предлагает богатый набор матчеров аргументов, которые позволяют точно указать, при каких условиях должно быть выброшено исключение:
// Выброс исключения только для определенного значения аргумента
doThrow(SecurityException.class).when(securityService)
.authorizeUser(eq("restricted_user"));
// Выброс исключения при соответствии аргумента определенному условию
doThrow(ValidationException.class).when(validator)
.validateData(argThat(data -> data == null || data.isEmpty()));
// Комбинирование матчеров
doThrow(DatabaseException.class).when(repository)
.save(argThat(entity -> entity.getId() == null), eq(SaveMode.STRICT));
4. Тестируйте каскадные исключения и их преобразования
В многослойной архитектуре исключения часто преобразуются при прохождении через слои. Тестирование должно проверять корректность этих преобразований:
// Настраиваем выброс низкоуровневого исключения
doThrow(new SQLException("DB connection failed")).when(dataSource)
.executeQuery(anyString());
// Проверяем, что сервисный слой обернул его в бизнес-исключение
ServiceException exception = assertThrows(ServiceException.class, () -> {
userService.getUserData(userId);
});
// Проверяем правильность цепочки исключений
assertTrue(exception.getCause() instanceof SQLException);
assertTrue(exception.getMessage().contains("Failed to retrieve user data"));
5. Применяйте последовательные исключения для сложных сценариев
Иногда требуется проверить поведение системы при последовательных вызовах метода с разными результатами:
// Первые два вызова выбрасывают исключение, третий — нет
doThrow(TimeoutException.class, TimeoutException.class)
.doNothing()
.when(networkService).connect();
// Проверяем успешное подключение после повторных попыток
retryableService.connectWithRetry();
// Проверяем, что было сделано ровно 3 попытки
verify(networkService, times(3)).connect();
6. Создавайте специализированные матчеры для сложных условий
Для сложной логики проверки аргументов полезно создавать специализированные матчеры:
class InvalidUserMatcher implements ArgumentMatcher<User> {
@Override
public boolean matches(User user) {
return user != null &&
(user.getUsername() == null ||
user.getUsername().length() < 3 ||
user.getEmail() == null);
}
}
// Использование в тесте
doThrow(ValidationException.class).when(validator)
.validateUser(argThat(new InvalidUserMatcher()));
7. Оптимизируйте проверки для однократных исключений
Для случаев, когда исключение должно быть выброшено только один раз, можно использовать комбинацию doThrow() и doNothing():
// Первый вызов выбрасывает исключение, последующие — нет
doThrow(ServiceUnavailableException.class).doNothing()
.when(service).performOperation();
// Первый вызов приводит к исключению
assertThrows(ServiceUnavailableException.class, () -> {
service.performOperation();
});
// Второй вызов успешен
service.performOperation(); // Не выбрасывает исключение
8. Тестируйте поведение системы при выбросе исключения
Помимо проверки самого факта выброса исключения, важно тестировать, что система корректно обрабатывает этот случай:
// Настраиваем мок для выброса исключения
doThrow(IOException.class).when(fileSystem).writeData(any(), any());
// Выполняем операцию, которая должна обработать исключение
boolean result = resilientService.trySaveData("filename", "data".getBytes());
// Проверяем, что операция корректно вернула false, а не пропустила исключение
assertFalse(result);
// Проверяем, что система выполнила все необходимые действия при ошибке
verify(logger).logError(anyString(), any(IOException.class));
verify(metricCollector).incrementCounter("save_failures");
Следуя этим практикам, вы сможете создавать надежные и информативные тесты для проверки корректного поведения void-методов при выбросе исключений, что является критически важной составляющей общей стратегии тестирования.
Тестирование void-методов в Mockito с проверкой исключений — это важный навык для обеспечения надежности программного обеспечения. Использование
doThrow().when()вместо традиционногоwhen().thenThrow()позволяет элегантно обойти ограничения void-методов и создать эффективные тесты. Помните, что тщательное тестирование исключительных ситуаций — это не просто дополнение, а фундаментальная часть процесса разработки, которая помогает предотвратить сбои в боевых условиях. Применяя рассмотренные практики, вы значительно повысите качество своего кода и сократите количество непредвиденных ошибок в продакшене.