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 не може ово да провери јер не извршава стварну логику tracker-а.

СценаријSpyMock
Стварна логика потребнаДаНе (заглушка)
Верификација позиваДа (број, аргументи)Да (број, аргументи)
Делимично стабловањеДа (неки методи — spy, други — stub)Не (сви методи — заглушке)
Ризик од споредних ефекатаВисок (стварни код)Нула
БрзинаМања (стварна логика)Већа (заглушке)
ЧитљивостМања (теже разумети шта је стварно)Већа (све експлицитно)

Антиобразац: spy за све

Spy за све — користити spy уместо mock-а за све тестове је грешка. Spy извршава стварни код који може имати споредне ефекте: упис у датотеку, слање HTTP-а, промена глобалног стања. Ако тестирани модул позива метод spy-објекта који шаље HTTP захтев, тест постаје интеграциони, а не јединични тест. Правило: ако spy омотава објекат са I/O операцијама — то више није јединични тест. Користите 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) или користити интерфејс.

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 креира ручно. Тест структура имплементира протокол и изнутра позива стварни објекат, истовремено бележећи позиве. Ово је више кода, али потпуно контролисано и type-safe. Ручни шпијуни не захтевају спољне библиотеке и не користе 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 пружа потпуну контролу над бележењем позива, не захтева рефлексију и ради са value-типовима (struct). Једини недостатак: потребно је одржавати код spy класе синхронизованим са протоколом при додавању нових метода.

Типични сценарији примене Spy

Провера аналитике — најчешћи сценариј употребе spy-а. У продукцијском коду, позиви аналитике су раштркани по целој апликацији: login, logout, purchase, error. Тест креира spy-омотач за AnalyticsTracker, извршава сценариј (пријава, преглед производа, додавање у корпу, куповина) и проверава да су сви потребни догађаји послати у правилном редоследу. Mock није прикладан јер AnalyticsTracker садржи логику баферисања и слања.

Тајмери и планирачи — тестирање кода који користи Handler (Android) или Timer (iOS) је тешко због стварног времена. Spy-омотач за Scheduler бележи који задаци су планирани и са којим кашњењем. Тест креира spy стварног Handler-а, извршава акцију и проверава да ли је Handler.postDelayed(runnable, delay) позван са правилним кашњењем. Стварни задатак се при томе не извршава — spy пресреће и бележи позив.

Логовање и debug информације — у продукцији, логови могу бити искључени или уписани у датотеку. 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 или чита са диска — тест престаје да буде јединични тест. Други случај: тест проверава само повратну вредност без интересовања за позиве — овде је довољан stub, а spy је сувишан. Трећи: код се ослања на унутрашње стање spy-а — ово је крхак тест.

Да ли MockK подржава spy?

Да, кроз spyk() — аналогно Mockito.spy(). MockK.spyk() ствара шпијуна око стварног објекта, подржава every за делимично стабловање и coVerify/coroutinesVerify за suspend функције. Ограничење: не ради са final класама (потребан open или интерфејс). За 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 операцијама претвара јединични тест у интеграциони

Развићемо мобилну апликацију под кључ

IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође