Spy (шпион) — тестовый объект, который оборачивает реальный экземпляр и записывает информацию о каждом вызове: какие методы вызывались, с какими аргументами, сколько раз. В отличие от mock, spy использует реальную реализацию обёрнутого объекта — вызовы проходят через настоящий код, а spy только фиксирует факты. После выполнения теста разработчик проверяет записи шпиона: «Метод sendAnalytics был вызван трижды?». Подробнее — в руководстве Android по тестированию.
Главное
Spy — это обёртка вокруг реального объекта, которая перехватывает все вызовы методов и записывает их. Реальная логика объекта выполняется: если метод сохраняет данные, вычисляет значение или делает запрос — всё происходит как обычно. Дополнительно spy фиксирует метаданные: имя метода, аргументы, количество вызовов, время выполнения. Термин входит в классификацию Meszaros (2007) и детально описан в статье Martin Fowler «Mocks Aren't Stubs».
Ключевое отличие от Mock — mock полностью заменяет объект тестовой заглушкой, все методы по умолчанию ничего не делают. Spy оборачивает существующий объект: все методы по умолчанию работают как обычно, но при этом записываются. Это различие принципиально: mock изолирует тестируемый код от реальности, spy сохраняет реальность и даёт наблюдать за ней. Выбор между ними зависит от того, что тестируется.
Spy — правильный выбор — если тестируемый код модифицирует состояние реального объекта, а тест должен и проверить результат (состояние), и убедиться, что вызовы были сделаны в правильном порядке. Mock не подходит, потому что он не выполняет реальную реализацию. Stub не подходит, потому что не записывает вызовы. Spy — единственный test double, который одновременно сохраняет реальную логику и предоставляет информацию о вызовах.
Mock — полная изоляция. Если тест не должен зависеть от реализации реального объекта (например, база данных или сетевой клиент), используйте mock. Mock гарантирует, что ни один вызов не дойдёт до реального компонента. Это безопасно и предсказуемо. Недостаток: mock не выполняет реальную логику, поэтому если тестируемый код полагается на возвращаемое значение — его нужно настраивать явно через when/stub.
Spy — реальная логика + наблюдение. Если тестируемый код взаимодействует с объектом, чья логика важна для теста, а не просто данные — используйте spy. Например, AnalyticsTracker, который собирает события и периодически отправляет их. Тест проверяет, что события добавились в буфер, а после отправки буфер очистился. Mock не может этого проверить, потому что не выполняет реальную логику трекера.
| Сценарий | Spy | Mock |
|---|---|---|
| Реальная логика нужна | Да | Нет (заглушка) |
| Верификация вызовов | Да (количество, аргументы) | Да (количество, аргументы) |
| Частичное стабление | Да (одни методы — spy, другие — stub) | Нет (все методы — заглушки) |
| Риск побочных эффектов | Высокий (реальный код) | Нулевой |
| Скорость | Ниже (реальная логика) | Выше (заглушки) |
| Читаемость | Ниже (сложнее понять, что реально) | Выше (всё явно) |
Spy для всего — использовать spy вместо mock для всех тестов — ошибка. Spy выполняет реальный код, который может иметь побочные эффекты: запись в файл, отправка HTTP, изменение глобального состояния. Если тестируемый модуль вызывает метод spy-объекта, который делает HTTP-запрос, тест становится интеграционным, а не unit-тестом. Правило: если spy обёртывает объект с I/O-операциями — это уже не unit-тест. Используйте mock для изоляции I/O, spy — только для in-memory объектов без внешних эффектов.
Mockito.spy() — классический способ создания шпиона в Java/Kotlin-проектах. spy() принимает реальный объект и возвращает обёртку. Все вызовы по умолчанию делегируются реальному объекту, а результаты записываются. После выполнения теста через verify() можно проверить количество вызовов и аргументы. Для методов, которые должны возвращать тестовые данные, используется doReturn/when — это называется «частичное стабление» (partial mocking).
class AnalyticsReporterTest {
private val realTracker = AnalyticsTracker()
private val spyTracker = Mockito.spy(realTracker)
fun test_event_tracked() {
val event = AnalyticsEvent("login")
spyTracker.track(event)
Mockito.verify(spyTracker).track(event)
assertEquals(1, spyTracker.getBufferedCount())
}
fun test_track_with_exception() {
Mockito.doThrow(RuntimeException("network"))
.when(spyTracker).flush()
spyTracker.track(AnalyticsEvent("login"))
assertTrue(spyTracker.hasPendingEvents())
}
}
MockK.spyk() — альтернатива для Kotlin-проектов с лучшей поддержкой корутин и sealed-классов. MockK.spyk() создаёт шпион, аналог Mockito.spy(). Поддерживает coVerify для suspend-функций и every для частичного стабления. В отличие от Mockito, MockK не поддерживает spy для final-классов (все классы в Kotlin final по умолчанию) — нужно либо открывать класс (open), либо использовать interface.
class LoginUseCaseTest {
private val realRepo = UserRepository()
private val spyRepo = spyk(realRepo)
private val useCase = LoginUseCase(spyRepo)
fun test_login_calls_save() = runTest {
every { spyRepo.getUser(any()) } returns User("test")
val result = useCase.login("test", "pass")
coVerify { spyRepo.saveLoginTime(any()) }
assertTrue(result.isSuccess)
}
}
Частичное стабление — мощный, но опасный приём. Вы можете сделать spy объекта и переопределить (stub) только некоторые методы, оставив остальные реальными. Пример: spy-репозиторий, у которого getUser() возвращает тестовые данные, а saveUser() реально сохраняет в in-memory список. Это позволяет комбинировать преимущества stub (контролируемые данные) и spy (реальная логика). Недостаток: сложность чтения теста — неочевидно, какие методы реальны, а какие — stub.
OCMock для Objective-C — библиотека, поддерживающая создание spy-объектов через niceMock. OCMock перехватывает вызовы методов с помощью Objective-C runtime и записывает их. После выполнения теста вызывается verify. OCMock поддерживает spy для любого объекта (в Objective-C все методы динамические), что даёт преимущество перед Swift, где spy возможен только через протоколы.
// Создание spy для реального объекта
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];
// Выполнение теста
[spy trackEvent:@"login"];
// Верификация
[[spy verify] trackEvent:@"login"];
XCTAssertEqual([realTracker eventCount], 1);
Swift protocol-based spy — в Swift нет runtime-рефлексии Objective-C, поэтому spy создаётся вручную. Тестовая структура реализует протокол и внутри вызывает реальный объект, одновременно записывая вызовы. Это больше кода, но полностью контролируемо и типобезопасно. Ручные шпионы не требуют внешних библиотек и не используют runtime — всё проверяется на этапе компиляции.
protocol AnalyticsProtocol {
func trackEvent(name: String)
}
final class SpyAnalytics: AnalyticsProtocol {
private let real: AnalyticsProtocol
private var events: [String] = []
init(real: AnalyticsProtocol) {
self.real = real
}
func trackEvent(name: String) {
events.append(name)
real.trackEvent(name: name)
}
func verifyTracked(name: String) -> Bool {
return events.contains(name)
}
}
Когда использовать OCMock vs ручной spy — для Objective-C-кода используйте OCMock (меньше boilerplate). Для Swift — предпочтительны ручные шпионы через протоколы. Ручной spy даёт полный контроль над записью вызовов, не требует reflection и работает с value-типами (struct). Единственный недостаток — нужно поддерживать код spy-класса синхронизированным с протоколом при добавлении новых методов.
Проверка аналитики — самый частый сценарий использования spy. В production-коде вызовы аналитики разбросаны по всему приложению: login, logout, purchase, error. Тест создаёт spy-обёртку для AnalyticsTracker, выполняет сценарий (логин, просмотр товара, добавление в корзину, покупка) и проверяет, что все нужные события были отправлены в правильном порядке. Mock не подходит, потому что AnalyticsTracker содержит логику буферизации и отправки.
Таймеры и планировщики — тестирование кода, использующего Handler (Android) или Timer (iOS), сложно из-за реального времени. Spy-обёртка для Scheduler фиксирует, какие задачи были запланированы и с какой задержкой. Тест создаёт spy реального Handler, выполняет действие и проверяет, что Handler.postDelayed(runnable, delay) был вызван с правильной задержкой. Реальная задача при этом не выполняется — spy перехватывает и записывает вызов.
Логирование и дебаг-информация — в production логи могут быть выключены или писать в файл. Spy-обёртка для Logger записывает все сообщения в in-memory список, который тест проверяет после выполнения. Это позволяет проверить, что при ошибке пишется корректное сообщение, без захламления консоли. Ручные шпионы для Logger особенно полезны на iOS, где OSLog не имеет тестового API.
Проверка последовательности вызовов — некоторые сценарии требуют строгого порядка операций: открыть соединение, отправить данные, закрыть соединение. Mockito позволяет проверить порядок через InOrder.verify(). Spy делает то же самое, но сохраняет реальное выполнение. Если важен не только порядок, но и результат каждого шага (соединение реально открылось) — используйте spy, а не mock.
Часто задаваемые вопросы
Spy оборачивает реальный объект и выполняет его логику, дополнительно записывая вызовы. Mock полностью замещает объект заглушкой — никакая реальная логика не выполняется. Spy сохраняет поведение, mock — нет. Выбирайте spy, когда важна реальная работа объекта; mock — когда нужно изолировать тест от внешней зависимости.
Когда spy-обёртка приводит к реальным I/O-операциям. Если spy обёртывает объект, который пишет в файл, отправляет HTTP или читает с диска — тест перестаёт быть unit-тестом. Второй случай: тест проверяет только возвращаемое значение без интереса к вызовам — здесь достаточно stub, а spy избыточен. Третий: код полагается на внутреннее состояние spy — это хрупкий тест.
Да, через spyk() — аналог Mockito.spy(). MockK.spyk() создаёт шпион вокруг реального объекта, поддерживает every для частичного стабления и coVerify/coroutinesVerify для suspend-функций. Ограничение: не работает с final-классами (нужен open или interface). Для Java-классов MockK также поддерживает spyk(), но требует аннотации @MockKJvmInline.
Технически — нет. Mock — это заглушка, не содержащая реальной реализации. Spy по определению обёртывает реальный объект. В Mockito нельзя превратить mock в spy. Но можно сделать наоборот: создать spy и переопределить часть методов через doReturn/when (partial mocking). Это даёт поведение, похожее на mock, для выбранных методов spy-объекта.
Обязательно. В Swift нет динамического проксирования как в Java/Kotlin. Для создания spy нужен протокол, который реализуют и продакшен-класс, и spy-класс. Swift-protocol-based spy — это ручная реализация, которая принимает реальный объект, делегирует ему вызовы и записывает метаданные. Альтернатива: библиотека Cuckoo, которая генерирует spy-классы через SourceKit.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также