Ошибки зависимостей: thenThrow и doThrow
Самые ценные тесты часто проверяют, что будет, когда что-то пошло не так: база недоступна, почтовый сервер не отвечает. С настоящими зависимостями такие ситуации трудно воспроизвести. Мок может выбросить любое исключение по команде — а тест проверяет, как на это реагирует сервис.
thenThrow
// src/test/java/com/example/blog/PostServiceErrorsTest.java
@Test
void databaseErrorReachesCaller() {
when(repository.save(any(Post.class)))
.thenThrow(new IllegalStateException("База данных недоступна"));
IllegalStateException e = assertThrows(IllegalStateException.class,
() -> service.create("Привет", "anna"));
assertEquals("База данных недоступна", e.getMessage());
}Мок репозитория выбрасывает исключение на save(), а тест проверяет, что create() не глотает ошибку, а передаёт её вызывающему коду. Как и раньше, repository, notifier и service — поля тестового класса.
doThrow для void-методов
notifier.send() возвращает void, и when(notifier.send(...)) не компилируется: void нельзя передать в метод. Для таких методов порядок обратный — сначала действие, потом мок и вызов:
// src/test/java/com/example/blog/PostServiceErrorsTest.java
@Test
void mailErrorAfterSave() {
Post draft = new Post(1L, "Привет", "anna", false);
when(repository.findById(1L)).thenReturn(Optional.of(draft));
doThrow(new IllegalStateException("Почтовый сервер не отвечает"))
.when(notifier).send(anyString(), anyString());
assertThrows(IllegalStateException.class, () -> service.publish(1L));
verify(repository).save(draft.publish());
}Тест показывает реальное свойство publish(): сначала пост сохраняется, а письмо отправляется потом. Если почта упала, пост уже опубликован, а пользователь получил ошибку. Правильно ли это — вопрос к разработчику, но тест сделал поведение видимым.
К тому же семейству относятся doReturn, doNothing и doAnswer; они нужны для void-методов и для шпионов (о них — в следующих уроках).
Последовательные вызовы
Несколько thenReturn / thenThrow в цепочке задают ответы на первый, второй и следующие вызовы:
// src/test/java/com/example/blog/PostServiceErrorsTest.java
@Test
void consecutiveCalls() {
when(repository.findById(1L))
.thenReturn(Optional.of(new Post(1L, "Привет", "anna", false)))
.thenReturn(Optional.empty());
assertEquals("Привет", service.find(1L).title());
assertThrows(PostNotFoundException.class, () -> service.find(1L));
}Первый вызов находит пост, второй — нет, будто его удалили между вызовами. Последний ответ повторяется для всех дальнейших вызовов.
Проверяемые исключения
Мок может выбросить только те исключения, которые метод действительно может выбросить. findById не объявляет IOException, поэтому такое Mockito отклонит:
when(repository.findById(anyLong())).thenThrow(new IOException("Диск переполнен"));org.mockito.exceptions.base.MockitoException:
Checked exception is invalid for this method!
Invalid: java.io.IOException: Диск переполненНепроверяемые исключения (RuntimeException и его наследники) разрешены всегда.
Частая ошибка
when для void-метода:
when(notifier.send(anyString(), anyString()))
.thenThrow(new IllegalStateException("Почтовый сервер не отвечает"));Тест даже не компилируется:
[ERROR] /.../src/test/java/com/example/blog/VoidWhenTest.java:[14,27] 'void' type not allowed hereДля void-методов используйте doThrow(...).when(notifier).send(...). Обратите внимание на скобки: when(notifier) получает мок, а метод вызывается уже после него.