Spy — что это, Mockito.spy() и верификация вызовов

Автор: IT Sectr Опубликовано: 2026-04-10 Время чтения: 9 мин

Spy (шпион) — тестовый объект, который оборачивает реальный экземпляр и записывает информацию о каждом вызове: какие методы вызывались, с какими аргументами, сколько раз. В отличие от mock, spy использует реальную реализацию обёрнутого объекта — вызовы проходят через настоящий код, а spy только фиксирует факты. После выполнения теста разработчик проверяет записи шпиона: «Метод sendAnalytics был вызван трижды?». Подробнее — в руководстве Android по тестированию.

Главное

  • Spy — объект, записывающий вызовы методов реальной реализации без её замены
  • Верификация — после теста spy позволяет проверить, сколько раз и с какими аргументами вызывался метод
  • Mockito.spy() — создаёт шпион на Android для реальных Java/Kotlin-объектов
  • Partial mocking — spy можно комбинировать со stub: одни методы перехватывать, другие пропускать
  • iOS — OCMock (Objective-C) и ручные шпионы через протоколы в Swift

Что такое Spy и чем отличается от Mock?

Spy — это обёртка вокруг реального объекта, которая перехватывает все вызовы методов и записывает их. Реальная логика объекта выполняется: если метод сохраняет данные, вычисляет значение или делает запрос — всё происходит как обычно. Дополнительно spy фиксирует метаданные: имя метода, аргументы, количество вызовов, время выполнения. Термин входит в классификацию Meszaros (2007) и детально описан в статье Martin Fowler «Mocks Aren't Stubs».

Ключевое отличие от Mock — mock полностью заменяет объект тестовой заглушкой, все методы по умолчанию ничего не делают. Spy оборачивает существующий объект: все методы по умолчанию работают как обычно, но при этом записываются. Это различие принципиально: mock изолирует тестируемый код от реальности, spy сохраняет реальность и даёт наблюдать за ней. Выбор между ними зависит от того, что тестируется.

Когда Spy — правильный выбор

Spy — правильный выбор — если тестируемый код модифицирует состояние реального объекта, а тест должен и проверить результат (состояние), и убедиться, что вызовы были сделаны в правильном порядке. Mock не подходит, потому что он не выполняет реальную реализацию. Stub не подходит, потому что не записывает вызовы. Spy — единственный test double, который одновременно сохраняет реальную логику и предоставляет информацию о вызовах.

Spy vs Mock: когда использовать шпион

Mock — полная изоляция. Если тест не должен зависеть от реализации реального объекта (например, база данных или сетевой клиент), используйте mock. Mock гарантирует, что ни один вызов не дойдёт до реального компонента. Это безопасно и предсказуемо. Недостаток: mock не выполняет реальную логику, поэтому если тестируемый код полагается на возвращаемое значение — его нужно настраивать явно через when/stub.

Spy — реальная логика + наблюдение. Если тестируемый код взаимодействует с объектом, чья логика важна для теста, а не просто данные — используйте spy. Например, AnalyticsTracker, который собирает события и периодически отправляет их. Тест проверяет, что события добавились в буфер, а после отправки буфер очистился. Mock не может этого проверить, потому что не выполняет реальную логику трекера.

СценарийSpyMock
Реальная логика нужнаДаНет (заглушка)
Верификация вызововДа (количество, аргументы)Да (количество, аргументы)
Частичное стаблениеДа (одни методы — spy, другие — stub)Нет (все методы — заглушки)
Риск побочных эффектовВысокий (реальный код)Нулевой
СкоростьНиже (реальная логика)Выше (заглушки)
ЧитаемостьНиже (сложнее понять, что реально)Выше (всё явно)

Антипаттерн: spy для всего

Spy для всего — использовать spy вместо mock для всех тестов — ошибка. Spy выполняет реальный код, который может иметь побочные эффекты: запись в файл, отправка HTTP, изменение глобального состояния. Если тестируемый модуль вызывает метод spy-объекта, который делает HTTP-запрос, тест становится интеграционным, а не unit-тестом. Правило: если spy обёртывает объект с I/O-операциями — это уже не unit-тест. Используйте mock для изоляции I/O, spy — только для in-memory объектов без внешних эффектов.

Mockito.spy() и spy в MockK на Android

Mockito.spy() — классический способ создания шпиона в Java/Kotlin-проектах. spy() принимает реальный объект и возвращает обёртку. Все вызовы по умолчанию делегируются реальному объекту, а результаты записываются. После выполнения теста через verify() можно проверить количество вызовов и аргументы. Для методов, которые должны возвращать тестовые данные, используется doReturn/when — это называется «частичное стабление» (partial mocking).

kotlin
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.

kotlin
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

Частичное стабление — мощный, но опасный приём. Вы можете сделать spy объекта и переопределить (stub) только некоторые методы, оставив остальные реальными. Пример: spy-репозиторий, у которого getUser() возвращает тестовые данные, а saveUser() реально сохраняет в in-memory список. Это позволяет комбинировать преимущества stub (контролируемые данные) и spy (реальная логика). Недостаток: сложность чтения теста — неочевидно, какие методы реальны, а какие — stub.

Реализация Spy на iOS с OCMock и протоколами

OCMock для Objective-C — библиотека, поддерживающая создание spy-объектов через niceMock. OCMock перехватывает вызовы методов с помощью Objective-C runtime и записывает их. После выполнения теста вызывается verify. OCMock поддерживает spy для любого объекта (в Objective-C все методы динамические), что даёт преимущество перед Swift, где spy возможен только через протоколы.

objective-c
// Создание 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 — всё проверяется на этапе компиляции.

swift
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

Проверка аналитики — самый частый сценарий использования 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 vs Mock: в чём основная разница?

Spy оборачивает реальный объект и выполняет его логику, дополнительно записывая вызовы. Mock полностью замещает объект заглушкой — никакая реальная логика не выполняется. Spy сохраняет поведение, mock — нет. Выбирайте spy, когда важна реальная работа объекта; mock — когда нужно изолировать тест от внешней зависимости.

Когда Spy — плохой выбор?

Когда spy-обёртка приводит к реальным I/O-операциям. Если spy обёртывает объект, который пишет в файл, отправляет HTTP или читает с диска — тест перестаёт быть unit-тестом. Второй случай: тест проверяет только возвращаемое значение без интереса к вызовам — здесь достаточно stub, а spy избыточен. Третий: код полагается на внутреннее состояние spy — это хрупкий тест.

Поддерживает ли MockK spy?

Да, через spyk() — аналог Mockito.spy(). MockK.spyk() создаёт шпион вокруг реального объекта, поддерживает every для частичного стабления и coVerify/coroutinesVerify для suspend-функций. Ограничение: не работает с final-классами (нужен open или interface). Для Java-классов MockK также поддерживает spyk(), но требует аннотации @MockKJvmInline.

Можно ли сделать Spy из Mock?

Технически — нет. Mock — это заглушка, не содержащая реальной реализации. Spy по определению обёртывает реальный объект. В Mockito нельзя превратить mock в spy. Но можно сделать наоборот: создать spy и переопределить часть методов через doReturn/when (partial mocking). Это даёт поведение, похожее на mock, для выбранных методов spy-объекта.

Spy в Swift — обязательно через протокол?

Обязательно. В Swift нет динамического проксирования как в Java/Kotlin. Для создания spy нужен протокол, который реализуют и продакшен-класс, и spy-класс. Swift-protocol-based spy — это ручная реализация, которая принимает реальный объект, делегирует ему вызовы и записывает метаданные. Альтернатива: библиотека Cuckoo, которая генерирует spy-классы через SourceKit.

Итоги

  • Spy — обёртка вокруг реального объекта, записывающая все вызовы методов без замены логики
  • Отличие от Mock — spy выполняет реальный код, mock заменяет его заглушкой
  • Mockito.spy() — для Java/Kotlin, обёртывает реальный объект с возможностью verify и partial mock
  • MockK.spyk() — Kotlin-аналог с поддержкой корутин, sealed-классов и coVerify
  • iOS — OCMock для Objective-C, ручные protocol-based spy для Swift
  • Основной сценарий — проверка аналитики, таймеров, логов и последовательности вызовов
  • Осторожно — spy с I/O-операциями превращает unit-тест в интеграционный

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также