Spy — co to jest, Mockito.spy() i weryfikacja wywołań

Autor: IT Sectr Opublikowano: 2026-04-10 Czas czytania: 9 min

Spy (szpieg) — obiekt testowy, który opakowuje rzeczywistą instancję i rejestruje informacje o każdym wywołaniu: które metody zostały wywołane, z jakimi argumentami, ile razy. W przeciwieństwie do mocka, spy używa rzeczywistej implementacji opakowanego obiektu — wywołania przechodzą przez prawdziwy kod, a spy tylko rejestruje fakty. Po wykonaniu testu programista sprawdza zapisy szpiega: „Czy metoda sendAnalytics została wywołana trzy razy?”. Więcej — w przewodniku Androida po testowaniu.

Najważniejsze

  • Spy — obiekt rejestrujący wywołania metod rzeczywistej implementacji bez jej zastępowania
  • Weryfikacja — po teście spy pozwala sprawdzić, ile razy i z jakimi argumentami metoda została wywołana
  • Mockito.spy() — tworzy szpiega na Androidzie dla rzeczywistych obiektów Java/Kotlin
  • Partial mocking — spy można łączyć ze stubem: jedne metody przechwytywać, inne przepuszczać
  • iOS — OCMock (Objective-C) i ręczni szpiedzy przez protokoły w Swift

Czym jest Spy i czym różni się od Mocka?

Spy — to opakowanie wokół rzeczywistego obiektu, które przechwytuje wszystkie wywołania metod i rejestruje je. Rzeczywista logika obiektu jest wykonywana: jeśli metoda zapisuje dane, oblicza wartość lub wykonuje zapytanie — wszystko dzieje się jak zwykle. Dodatkowo spy rejestruje metadane: nazwę metody, argumenty, liczbę wywołań, czas wykonania. Termin wchodzi w skład klasyfikacji Meszaros (2007) i został szczegółowo opisany w artykule Martina Fowlera „Mocks Aren't Stubs”.

Kluczowa różnica od Mocka — mock całkowicie zastępuje obiekt testową zaślepką, wszystkie metody domyślnie nic nie robią. Spy opakowuje istniejący obiekt: wszystkie metody domyślnie działają jak zwykle, ale przy tym są rejestrowane. Ta różnica jest zasadnicza: mock izoluje testowany kod od rzeczywistości, spy zachowuje rzeczywistość i pozwala ją obserwować. Wybór między nimi zależy od tego, co jest testowane.

Kiedy Spy jest właściwym wyborem

Spy — właściwy wybór — jeśli testowany kod modyfikuje stan rzeczywistego obiektu, a test ma zarówno sprawdzić wynik (stan), jak i upewnić się, że wywołania zostały wykonane w prawidłowej kolejności. Mock nie pasuje, ponieważ nie wykonuje rzeczywistej implementacji. Stub nie pasuje, ponieważ nie rejestruje wywołań. Spy jest jedynym test double, który jednocześnie zachowuje rzeczywistą logikę i dostarcza informacji o wywołaniach.

Spy vs Mock: kiedy używać szpiega

Mock — pełna izolacja. Jeśli test nie powinien zależeć od implementacji rzeczywistego obiektu (na przykład baza danych lub klient sieciowy), używaj mocka. Mock gwarantuje, że żadne wywołanie nie dotrze do rzeczywistego komponentu. To bezpieczne i przewidywalne. Wadą jest to, że mock nie wykonuje rzeczywistej logiki, więc jeśli testowany kod polega na wartości zwracanej — trzeba ją jawnie skonfigurować przez when/stub.

Spy — rzeczywista logika + obserwacja. Jeśli testowany kod współdziała z obiektem, którego logika jest ważna dla testu, a nie tylko dane — używaj spy. Na przykład AnalyticsTracker, który zbiera zdarzenia i okresowo je wysyła. Test sprawdza, czy zdarzenia zostały dodane do bufora, a po wysłaniu bufor został wyczyszczony. Mock nie może tego sprawdzić, ponieważ nie wykonuje rzeczywistej logiki trackera.

ScenariuszSpyMock
Rzeczywista logika potrzebnaTakNie (zaślepka)
Weryfikacja wywołańTak (liczba, argumenty)Tak (liczba, argumenty)
Częściowe stubbowanieTak (jedne metody — spy, inne — stub)Nie (wszystkie metody — zaślepki)
Ryzyko efektów ubocznychWysokie (rzeczywisty kod)Zerowe
SzybkośćNiższa (rzeczywista logika)Wyższa (zaślepki)
CzytelnośćNiższa (trudniej zrozumieć, co jest rzeczywiste)Wyższa (wszystko jawne)

Antywzorzec: spy do wszystkiego

Spy do wszystkiego — używanie spy zamiast mocka we wszystkich testach to błąd. Spy wykonuje rzeczywisty kod, który może mieć efekty uboczne: zapis do pliku, wysyłanie HTTP, zmiana stanu globalnego. Jeśli testowany moduł wywołuje metodę spy-obiektu, która wykonuje żądanie HTTP, test staje się integracyjny, a nie jednostkowy. Zasada: jeśli spy opakowuje obiekt z operacjami I/O — to już nie jest test jednostkowy. Używaj mocka do izolacji I/O, spy — tylko do obiektów in-memory bez efektów zewnętrznych.

Mockito.spy() i spy w MockK na Androidzie

Mockito.spy() — klasyczny sposób tworzenia szpiega w projektach Java/Kotlin. spy() przyjmuje rzeczywisty obiekt i zwraca opakowanie. Wszystkie wywołania domyślnie są delegowane do rzeczywistego obiektu, a wyniki są rejestrowane. Po wykonaniu testu przez verify() można sprawdzić liczbę wywołań i argumenty. Dla metod, które powinny zwracać dane testowe, używa się doReturn/when — nazywa się to „częściowym stubbowaniem” (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() — alternatywa dla projektów Kotlin z lepszym wsparciem dla korutyn i sealed-klas. MockK.spyk() tworzy szpiega, odpowiednik Mockito.spy(). Obsługuje coVerify dla suspend-funkcji i every do częściowego stubbowania. W przeciwieństwie do Mockito, MockK nie obsługuje spy dla final-klas (wszystkie klasy w Kotlin są domyślnie final) — trzeba albo otworzyć klasę (open), albo użyć interfejsu.

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)
    }
}

Cięściowe stubbowanie przez spy

Cięściowe stubbowanie — potężna, ale niebezpieczna technika. Możesz zrobić spy obiektu i nadpisać (stub) tylko niektóre metody, pozostawiając pozostałe rzeczywistymi. Przykład: spy-repozytorium, którego getUser() zwraca dane testowe, a saveUser() rzeczywiście zapisuje do listy in-memory. Pozwala to łączyć zalety stubów (kontrolowane dane) i spy (rzeczywista logika). Wadą jest złożoność czytania testu — nie jest oczywiste, które metody są rzeczywiste, a które są stubem.

Implementacja Spy na iOS z OCMock i protokołami

OCMock dla Objective-C — biblioteka obsługująca tworzenie spy-obiektów przez niceMock. OCMock przechwytuje wywołania metod za pomocą środowiska uruchomieniowego Objective-C i rejestruje je. Po wykonaniu testu wywoływana jest verify. OCMock obsługuje spy dla dowolnego obiektu (w Objective-C wszystkie metody są dynamiczne), co daje przewagę nad Swift, gdzie spy jest możliwy tylko przez protokoły.

objective-c
// Tworzenie spy dla rzeczywistego obiektu
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];

// Wykonanie testu
[spy trackEvent:@"login"];

// Weryfikacja
[[spy verify] trackEvent:@"login"];
XCTAssertEqual([realTracker eventCount], 1);

Swift protocol-based spy — w Swift nie ma refleksji środowiska uruchomieniowego Objective-C, więc spy jest tworzony ręcznie. Struktura testowa implementuje protokół i wewnętrznie wywołuje rzeczywisty obiekt, jednocześnie rejestrując wywołania. To więcej kodu, ale w pełni kontrolowalne i bezpieczne typowo. Ręczni szpiedzy nie wymagają zewnętrznych bibliotek i nie używają środowiska uruchomieniowego — wszystko jest sprawdzane na etapie kompilacji.

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)
    }
}

Kiedy używać OCMock vs ręczny spy — dla kodu Objective-C używaj OCMock (mniej boilerplate). Dla Swifta — preferowani są ręczni szpiedzy przez protokoły. Ręczny spy daje pełną kontrolę nad rejestracją wywołań, nie wymaga refleksji i działa z typami wartości (struct). Jedyną wadą jest konieczność utrzymywania kodu spy-klasy zsynchronizowanego z protokołem przy dodawaniu nowych metod.

Typowe scenariusze zastosowania Spy

Sprawdzanie analityki — najczęstszy scenariusz użycia spy. W kodzie produkcyjnym wywołania analityki są rozrzucone po całej aplikacji: login, logout, purchase, error. Test tworzy spy-opakowanie dla AnalyticsTracker, wykonuje scenariusz (logowanie, przeglądanie produktu, dodanie do koszyka, zakup) i sprawdza, czy wszystkie potrzebne zdarzenia zostały wysłane w prawidłowej kolejności. Mock nie pasuje, ponieważ AnalyticsTracker zawiera logikę buforowania i wysyłania.

Timery i planiści — testowanie kodu używającego Handler (Android) lub Timer (iOS) jest trudne ze względu na rzeczywisty czas. Spy-opakowanie dla Scheduler rejestruje, które zadania zostały zaplanowane i z jakim opóźnieniem. Test tworzy spy rzeczywistego Handlera, wykonuje akcję i sprawdza, czy Handler.postDelayed(runnable, delay) został wywołany z prawidłowym opóźnieniem. Rzeczywiste zadanie przy tym nie jest wykonywane — spy przechwytuje i rejestruje wywołanie.

Logowanie i informacje debugowe — w produkcji logi mogą być wyłączone lub zapisywane do pliku. Spy-opakowanie dla Loggera zapisuje wszystkie wiadomości na liście in-memory, którą test sprawdza po wykonaniu. Pozwala to sprawdzić, czy przy błędzie zapisywana jest poprawna wiadomość, bez zaśmiecania konsoli. Ręczni szpiedzy dla Loggera są szczególnie przydatni na iOS, gdzie OSLog nie ma testowego API.

Sprawdzanie kolejności wywołań — niektóre scenariusze wymagają ścisłej kolejności operacji: otwórz połączenie, wyślij dane, zamknij połączenie. Mockito pozwala sprawdzić kolejność przez InOrder.verify(). Spy robi to samo, ale zachowuje rzeczywiste wykonanie. Jeśli ważna jest nie tylko kolejność, ale także wynik każdego kroku (połączenie rzeczywiście zostało otwarte) — używaj spy, a nie mocka.

Często zadawane pytania

Spy vs Mock: jaka jest główna różnica?

Spy opakowuje rzeczywisty obiekt i wykonuje jego logikę, dodatkowo rejestrując wywołania. Mock całkowicie zastępuje obiekt zaślepką — żadna rzeczywista logika nie jest wykonywana. Spy zachowuje zachowanie, mock — nie. Wybieraj spy, gdy ważna jest rzeczywista praca obiektu; mock — gdy trzeba odizolować test od zewnętrznej zależności.

Kiedy Spy jest złym wyborem?

Gdy spy-opakowanie prowadzi do rzeczywistych operacji I/O. Jeśli spy opakowuje obiekt, który zapisuje do pliku, wysyła HTTP lub czyta z dysku — test przestaje być testem jednostkowym. Drugi przypadek: test sprawdza tylko wartość zwracaną bez zainteresowania wywołaniami — tutaj wystarczy stub, a spy jest zbędny. Trzeci: kod polega na wewnętrznym stanie spy — to kruchy test.

Czy MockK obsługuje spy?

Tak, przez spyk() — odpowiednik Mockito.spy(). MockK.spyk() tworzy szpiega wokół rzeczywistego obiektu, obsługuje every do częściowego stubbowania i coVerify/coroutinesVerify dla suspend-funkcji. Ograniczenie: nie działa z final-klasami (potrzebny open lub interfejs). Dla klas Java MockK również obsługuje spyk(), ale wymaga adnotacji @MockKJvmInline.

Czy można zrobić Spy z Mocka?

Technicznie — nie. Mock to zaślepka, niezawierająca rzeczywistej implementacji. Spy z definicji opakowuje rzeczywisty obiekt. W Mockito nie można zamienić mocka w spy. Ale można zrobić odwrotnie: stworzyć spy i nadpisać część metod przez doReturn/when (partial mocking). Daje to zachowanie podobne do mocka dla wybranych metod spy-obiektu.

Spy w Swift — koniecznie przez protokół?

Koniecznie. W Swift nie ma dynamicznego proxyowania jak w Java/Kotlin. Do stworzenia spy potrzebny jest protokół, który implementują zarówno klasa produkcyjna, jak i klasa spy. Swift-protocol-based spy to ręczna implementacja, która przyjmuje rzeczywisty obiekt, deleguje mu wywołania i rejestruje metadane. Alternatywa: biblioteka Cuckoo, która generuje spy-klasy przez SourceKit.

Podsumowanie

  • Spy — opakowanie wokół rzeczywistego obiektu, rejestrujące wszystkie wywołania metod bez zastępowania logiki
  • Różnica od Mocka — spy wykonuje rzeczywisty kod, mock zastępuje go zaślepką
  • Mockito.spy() — dla Java/Kotlin, opakowuje rzeczywisty obiekt z możliwością verify i partial mock
  • MockK.spyk() — Kotlin-odpowiednik z obsługą korutyn, sealed-klas i coVerify
  • iOS — OCMock dla Objective-C, ręczne protocol-based spy dla Swifta
  • Główny scenariusz — sprawdzanie analityki, timerów, logów i kolejności wywołań
  • Uwaga — spy z operacjami I/O zamienia test jednostkowy w integracyjny

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również