Шпионы
Мок — пустая заглушка: каждый метод возвращает пустое значение, пока вы не зададите ответ. Шпион (spy) — наоборот: он оборачивает настоящий объект, все методы работают как обычно, но Mockito записывает вызовы и позволяет подменить отдельные методы. Шпионы нужны редко — когда хочется сохранить настоящее поведение и проверить или изменить только небольшую его часть.
Шпион на настоящем репозитории
// src/test/java/com/example/blog/SpyTest.java
package com.example.blog;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
class SpyTest {
@Test
void spyCallsRealMethods() {
InMemoryPostRepository repository = spy(new InMemoryPostRepository());
PostService service = new PostService(repository, mock(Notifier.class));
Post post = service.create("Привет", "anna");
assertEquals(1L, post.id());
assertEquals(1, repository.findAll().size());
verify(repository).save(any(Post.class));
}
}spy(...) принимает готовый объект. Настоящий save() выдаёт id 1 и сохраняет пост — findAll() действительно возвращает один пост. При этом verify работает так же, как с моком.
Подмена одного метода
// src/test/java/com/example/blog/SpyTest.java
@Test
void spyCanOverrideOneMethod() {
InMemoryPostRepository repository = spy(new InMemoryPostRepository());
doReturn(Optional.of(new Post(7L, "Из шпиона", "oleg", false)))
.when(repository).findById(7L);
PostService service = new PostService(repository, mock(Notifier.class));
assertEquals("Из шпиона", service.find(7L).title());
}findById(7L) теперь возвращает подготовленный пост, все остальные методы остаются настоящими.
Мок или шпион
| Мок | Шпион | |
|---|---|---|
| Создаётся из | Класса или интерфейса | Готового объекта |
| Методы без настройки | Возвращают пустые значения | Вызывают настоящий код |
| Настройка ответа | when(...).thenReturn(...) | doReturn(...).when(spy).method(...) |
| Когда использовать | Почти всегда | Старый код, проверка вызовов на настоящем объекте |
Если хочется сделать шпиона из самого проверяемого класса — например, чтобы подменить один из его собственных методов, — обычно это признак того, что класс делает слишком много. Вынести эту часть в отдельную зависимость и подменить её моком — более чистое решение.
Частая ошибка
Со шпионом when(spy.method()) сначала вызывает настоящий метод — так Java вычисляет аргумент when. Если настоящий метод выбрасывает исключение, до подмены дело не доходит:
PostService service = spy(new PostService(new InMemoryPostRepository(), (to, message) -> { }));
when(service.find(7L)).thenReturn(new Post(7L, "Из шпиона", "oleg", false));[ERROR] com.example.blog.SpyWhenTest.spyWithWhen -- Time elapsed: 1.618 s <<< ERROR!
com.example.blog.PostNotFoundException: Пост не найден: 7
at com.example.blog.PostService.find(PostService.java:24)
at com.example.blog.SpyWhenTest.spyWithWhen(SpyWhenTest.java:16)Настоящий find(7L) заглянул в пустой репозиторий и выбросил исключение. Для шпионов всегда используйте форму doReturn(...).when(spy).method(...): она не вызывает настоящий метод.