Spy — wat het is, Mockito.spy() en verificatie van aanroepen

Auteur: IT Sectr Gepubliceerd: 2026-04-10 Leestijd: 9 min

Spy (spion) — een testobject dat een echte instantie omhult en informatie over elke aanroep registreert: welke methoden werden aangeroepen, met welke argumenten, hoe vaak. In tegenstelling tot mock gebruikt spy de echte implementatie van het omhulde object — aanroepen gaan door echte code en spy registreert alleen feiten. Na het uitvoeren van de test controleert de ontwikkelaar de registraties van de spion: „Is de methode sendAnalytics drie keer aangeroepen?”. Meer — in de Android-testhandleiding.

Belangrijkste punten

  • Spy — object dat aanroepen van methoden van de echte implementatie registreert zonder deze te vervangen
  • Verificatie — na de test kan spy controleren hoe vaak en met welke argumenten de methode is aangeroepen
  • Mockito.spy() — maakt een spion op Android voor echte Java/Kotlin-objecten
  • Partial mocking — spy kan worden gecombineerd met stub: sommige methoden onderscheppen, andere doorlaten
  • iOS — OCMock (Objective-C) en handmatige spionnen via protocollen in Swift

Wat is Spy en wat is het verschil met Mock?

Spy — is een omhulsel rond een echt object dat alle methodaanroepen onderschept en registreert. De echte logica van het object wordt uitgevoerd: als een methode gegevens opslaat, een waarde berekent of een verzoek doet — gebeurt alles zoals gewoonlijk. Daarnaast registreert spy metadata: de naam van de methode, argumenten, aantal aanroepen, uitvoeringstijd. De term maakt deel uit van de classificatie van Meszaros (2007) en wordt gedetailleerd beschreven in het artikel van Martin Fowler „Mocks Aren't Stubs”.

Belangrijkste verschil met Mock — mock vervangt het object volledig door een teststub, alle methoden doen standaard niets. Spy omhult een bestaand object: alle methoden werken standaard zoals gewoonlijk, maar worden tegelijkertijd geregistreerd. Dit verschil is fundamenteel: mock isoleert de geteste code van de realiteit, spy behoudt de realiteit en maakt het mogelijk deze te observeren. De keuze hangt af van wat er wordt getest.

Wanneer Spy de juiste keuze is

Spy — de juiste keuze — als de geteste code de toestand van een echt object wijzigt en de test zowel het resultaat (de toestand) moet controleren als ervoor moet zorgen dat aanroepen in de juiste volgorde zijn gedaan. Mock is niet geschikt omdat het de echte implementatie niet uitvoert. Stub is niet geschikt omdat het aanroepen niet registreert. Spy is de enige testdouble die tegelijkertijd de echte logica behoudt en informatie over aanroepen verschaft.

Spy vs Mock: wanneer gebruik je een spion

Mock — volledige isolatie. Als de test niet afhankelijk mag zijn van de implementatie van een echt object (bijvoorbeeld een database of netwerkclient), gebruik dan mock. Mock garandeert dat geen enkele aanroep de echte component bereikt. Dit is veilig en voorspelbaar. Nadeel: mock voert geen echte logica uit, dus als de geteste code afhankelijk is van de retourwaarde — moet deze expliciet worden geconfigureerd via when/stub.

Spy — echte logica + observatie. Als de geteste code interageert met een object waarvan de logica belangrijk is voor de test, niet alleen de gegevens — gebruik dan spy. Bijvoorbeeld AnalyticsTracker die gebeurtenissen verzamelt en periodiek verzendt. De test controleert of gebeurtenissen aan de buffer zijn toegevoegd en of de buffer na verzending is leeggemaakt. Mock kan dit niet controleren omdat het de echte logica van de tracker niet uitvoert.

ScenarioSpyMock
Echte logica nodigJaNee (stub)
Verificatie van aanroepenJa (aantal, argumenten)Ja (aantal, argumenten)
Gedeeltelijk stubbenJa (sommige methoden — spy, andere — stub)Nee (alle methoden — stubs)
Risico op bijwerkingenHoog (echte code)Nihil
SnelheidLager (echte logica)Hoger (stubs)
LeesbaarheidLager (moeilijker te begrijpen wat echt is)Hoger (alles expliciet)

Antipatroon: spy voor alles

Spy voor alles — spy gebruiken in plaats van mock voor alle tests is een fout. Spy voert echte code uit die bijwerkingen kan hebben: schrijven naar bestand, HTTP verzenden, wijzigen van globale toestand. Als de geteste module een methode van een spy-object aanroept die een HTTP-verzoek doet, wordt de test een integratietest in plaats van een unittest. Regel: als spy een object met I/O-operaties omhult — is dit geen unittest meer. Gebruik mock voor I/O-isolatie, spy — alleen voor in-memory objecten zonder externe effecten.

Mockito.spy() en spy in MockK op Android

Mockito.spy() — de klassieke manier om een spion te maken in Java/Kotlin-projecten. spy() ontvangt een echt object en retourneert een omhulsel. Alle aanroepen worden standaard gedelegeerd aan het echte object en de resultaten worden geregistreerd. Na het uitvoeren van de test kan via verify() het aantal aanroepen en de argumenten worden gecontroleerd. Voor methoden die testgegevens moeten retourneren wordt doReturn/when gebruikt — dit wordt „gedeeltelijk stubben” (partial mocking) genoemd.

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() — alternatief voor Kotlin-projecten met betere ondersteuning voor coroutines en sealed-klassen. MockK.spyk() maakt een spion, analoog aan Mockito.spy(). Ondersteunt coVerify voor suspend-functies en every voor gedeeltelijk stubben. In tegenstelling tot Mockito ondersteunt MockK geen spy voor final-klassen (alle klassen in Kotlin zijn standaard final) — de klasse moet worden geopend (open) of er moet een interface worden gebruikt.

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

Gedeeltelijk stubben via spy

Gedeeltelijk stubben — een krachtige maar gevaarlijke techniek. U kunt een spy van een object maken en slechts enkele methoden overschrijven (stubben), terwijl de andere echt blijven. Voorbeeld: een spy-repository waarvan getUser() testgegevens retourneert en saveUser() daadwerkelijk opslaat in een in-memory lijst. Dit maakt het mogelijk de voordelen van stubs (gecontroleerde gegevens) en spy (echte logica) te combineren. Nadeel: de complexiteit van het lezen van de test — het is niet duidelijk welke methoden echt zijn en welke stub zijn.

Implementatie van Spy op iOS met OCMock en protocollen

OCMock voor Objective-C — bibliotheek die het maken van spy-objecten via niceMock ondersteunt. OCMock onderschept methodaanroepen met behulp van de Objective-C-runtime en registreert ze. Na het uitvoeren van de test wordt verify aangeroepen. OCMock ondersteunt spy voor elk object (in Objective-C zijn alle methoden dynamisch), wat een voordeel biedt ten opzichte van Swift, waar spy alleen via protocollen mogelijk is.

objective-c
// Spy maken voor het echte object
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];

// Test uitvoeren
[spy trackEvent:@"login"];

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

Swift protocol-based spy — in Swift is er geen Objective-C-runtimereflectie, dus spy wordt handmatig gemaakt. De teststructuur implementeert het protocol en roept intern het echte object aan, terwijl het tegelijkertijd aanroepen registreert. Dit is meer code, maar volledig controleerbaar en typeveilig. Handmatige spionnen vereisen geen externe bibliotheken en gebruiken geen runtime — alles wordt gecontroleerd in de compilatiefase.

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

Wanneer OCMock vs handmatige spy gebruiken — gebruik voor Objective-C-code OCMock (minder boilerplate). Voor Swift — hebben handmatige spionnen via protocollen de voorkeur. Handmatige spy geeft volledige controle over de registratie van aanroepen, vereist geen reflectie en werkt met waardetypen (struct). Het enige nadeel: de spy-klassecode moet gesynchroniseerd blijven met het protocol bij het toevoegen van nieuwe methoden.

Typische toepassingsscenario's voor Spy

Controleren van analytics — het meest voorkomende scenario voor het gebruik van spy. In productiecode zijn analytics-aanroepen verspreid over de hele applicatie: login, logout, purchase, error. De test maakt een spy-omhulsel voor AnalyticsTracker, voert het scenario uit (inloggen, product bekijken, toevoegen aan winkelwagen, kopen) en controleert of alle benodigde gebeurtenissen in de juiste volgorde zijn verzonden. Mock is niet geschikt omdat AnalyticsTracker buffering- en verzendlogica bevat.

Timers en planners — het testen van code die Handler (Android) of Timer (iOS) gebruikt, is moeilijk vanwege de realtime. Spy-omhulsel voor Scheduler registreert welke taken zijn gepland en met welke vertraging. De test maakt een spy van de echte Handler, voert de actie uit en controleert of Handler.postDelayed(runnable, delay) met de juiste vertraging is aangeroepen. De echte taak wordt niet uitgevoerd — spy onderschept en registreert de aanroep.

Logging en debug-informatie — in productie kunnen logs zijn uitgeschakeld of naar een bestand worden geschreven. Spy-omhulsel voor Logger registreert alle berichten in een in-memory lijst die de test na uitvoering controleert. Dit maakt het mogelijk te controleren of bij een fout een correct bericht wordt geschreven, zonder de console te vervuilen. Handmatige spionnen voor Logger zijn vooral nuttig op iOS, waar OSLog geen test-API heeft.

Controleren van de volgorde van aanroepen — sommige scenario's vereisen een strikte volgorde van bewerkingen: verbinding openen, gegevens verzenden, verbinding sluiten. Mockito maakt het mogelijk de volgorde te controleren via InOrder.verify(). Spy doet hetzelfde maar behoudt de echte uitvoering. Als niet alleen de volgorde maar ook het resultaat van elke stap belangrijk is (de verbinding is echt geopend) — gebruik dan spy, niet mock.

Veelgestelde vragen

Spy vs Mock: wat is het belangrijkste verschil?

Spy omhult een echt object en voert zijn logica uit, met extra registratie van aanroepen. Mock vervangt het object volledig door een stub — er wordt geen echte logica uitgevoerd. Spy behoudt gedrag, mock niet. Kies spy wanneer het echte werk van het object belangrijk is; kies mock wanneer de test van een externe afhankelijkheid moet worden geïsoleerd.

Wanneer is Spy een slechte keuze?

Wanneer spy-omhulsel leidt tot echte I/O-operaties. Als spy een object omhult dat naar een bestand schrijft, HTTP verzendt of van schijf leest — is de test geen unittest meer. Tweede geval: de test controleert alleen de retourwaarde zonder interesse in aanroepen — hier is stub voldoende en is spy overbodig. Derde: de code vertrouwt op de interne toestand van spy — dit is een fragiele test.

Ondersteunt MockK spy?

Ja, via spyk() — analoog aan Mockito.spy(). MockK.spyk() maakt een spion rond een echt object, ondersteunt every voor gedeeltelijk stubben en coVerify/coroutinesVerify voor suspend-functies. Beperking: werkt niet met final-klassen (open of interface nodig). Voor Java-klassen ondersteunt MockK ook spyk(), maar vereist de @MockKJvmInline-annotatie.

Kan men een Spy maken van een Mock?

Technisch — nee. Mock is een stub zonder echte implementatie. Spy omhult per definitie een echt object. In Mockito kan een mock niet worden omgezet in een spy. Maar het omgekeerde is mogelijk: maak een spy en overschrijf een deel van de methoden via doReturn/when (partial mocking). Dit geeft gedrag vergelijkbaar met mock voor geselecteerde methoden van het spy-object.

Spy in Swift — verplicht via protocol?

Verplicht. In Swift is er geen dynamische proxy zoals in Java/Kotlin. Voor het maken van een spy is een protocol nodig dat zowel de productieklasse als de spy-klasse implementeren. Swift-protocol-based spy is een handmatige implementatie die een echt object ontvangt, er aanroepen aan delegeert en metadata registreert. Alternatief: de bibliotheek Cuckoo, die spy-klassen genereert via SourceKit.

Samenvatting

  • Spy — omhulsel rond een echt object dat alle methodaanroepen registreert zonder de logica te vervangen
  • Verschil met Mock — spy voert echte code uit, mock vervangt deze door een stub
  • Mockito.spy() — voor Java/Kotlin, omhult een echt object met mogelijkheid tot verify en partial mock
  • MockK.spyk() — Kotlin-analoog met ondersteuning voor coroutines, sealed-klassen en coVerify
  • iOS — OCMock voor Objective-C, handmatige protocol-based spy voor Swift
  • Belangrijkste scenario — controleren van analytics, timers, logs en volgorde van aanroepen
  • Let op — spy met I/O-operaties verandert een unittest in een integratietest

We ontwikkelen een mobiele applicatie turnkey

IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.

Bespreek het project

Lees ook