Spy — cos’è, Mockito.spy() e verifica delle chiamate

Autore: IT Sectr Pubblicato: 2026-04-10 Tempo di lettura: 9 min

Spy (spia) — un oggetto di test che avvolge un’istanza reale e registra informazioni su ogni chiamata: quali metodi sono stati chiamati, con quali argomenti e quante volte. A differenza di un mock, uno spy utilizza l’implementazione reale dell’oggetto avvolto — le chiamate passano attraverso il codice reale e lo spy registra solo i fatti. Dopo l’esecuzione del test, lo sviluppatore controlla le registrazioni della spia: “Il metodo sendAnalytics è stato chiamato tre volte?”. Ulteriori informazioni nella guida ai test Android.

Punti chiave

  • Spy — oggetto che registra le chiamate ai metodi di un’implementazione reale senza sostituirla
  • Verifica — dopo il test, uno spy permette di verificare quante volte e con quali argomenti un metodo è stato chiamato
  • Mockito.spy() — crea una spia su Android per oggetti Java/Kotlin reali
  • Partial mocking — uno spy può essere combinato con stub: intercettare alcuni metodi, lasciarne passare altri
  • iOS — OCMock (Objective-C) e spie manuali tramite protocolli in Swift

Cos’è uno Spy e in cosa differisce da un Mock?

Spy — un involucro attorno a un oggetto reale che intercetta tutte le chiamate ai metodi e le registra. La logica reale dell’oggetto viene eseguita: se il metodo salva dati, calcola un valore o effettua una richiesta — tutto avviene normalmente. Inoltre, lo spy registra metadati: nome del metodo, argomenti, numero di chiamate, tempo di esecuzione. Il termine fa parte della classificazione di Meszaros (2007) ed è descritto in dettaglio nell’articolo di Martin Fowler “Mocks Aren’t Stubs”.

Differenza fondamentale da un Mock — un mock sostituisce completamente l’oggetto con uno stub di test; tutti i metodi non fanno nulla per impostazione predefinita. Uno spy avvolge un oggetto esistente: tutti i metodi funzionano normalmente per impostazione predefinita, ma vengono anche registrati. Questa distinzione è fondamentale: un mock isola il codice testato dalla realtà, mentre uno spy preserva la realtà e permette di osservarla. La scelta tra di essi dipende da ciò che viene testato.

Quando uno Spy è la scelta giusta

Uno spy è la scelta giusta — se il codice testato modifica lo stato di un oggetto reale e il test deve sia verificare il risultato (stato) sia assicurarsi che le chiamate siano state effettuate nell’ordine corretto. Un mock non è adatto perché non esegue l’implementazione reale. Uno stub non è adatto perché non registra le chiamate. Uno spy è l’unico test double che preserva la logica reale e fornisce informazioni sulle chiamate.

Spy vs Mock: quando usare una spia

Mock — isolamento completo. Se il test non deve dipendere dall’implementazione dell’oggetto reale (ad esempio, un database o un client di rete), usa un mock. Un mock garantisce che nessuna chiamata raggiunga il componente reale. È sicuro e prevedibile. Lo svantaggio: un mock non esegue logica reale, quindi se il codice testato dipende da un valore restituito, deve essere configurato esplicitamente tramite when/stub.

Spy — logica reale + osservazione. Se il codice testato interagisce con un oggetto la cui logica è importante per il test, e non solo dati — usa uno spy. Ad esempio, un AnalyticsTracker che raccoglie eventi e li invia periodicamente. Il test verifica che gli eventi siano stati aggiunti al buffer e che dopo l’invio il buffer sia stato cancellato. Un mock non può verificarlo perché non esegue la logica reale del tracker.

ScenarioSpyMock
Logica reale necessariaNo (stub)
Verifica delle chiamateSì (numero, argomenti)Sì (numero, argomenti)
Stubbing parzialeSì (alcuni metodi spy, altri stub)No (tutti i metodi sono stub)
Rischio di effetti collateraliAlto (codice reale)Nullo
VelocitàInferiore (logica reale)Superiore (stub)
LeggibilitàInferiore (più difficile capire cosa è reale)Superiore (tutto è esplicito)

Antipattern: spy per tutto

Spy per tutto — usare uno spy invece di un mock per tutti i test è un errore. Uno spy esegue codice reale che può avere effetti collaterali: scrivere su file, inviare HTTP, modificare lo stato globale. Se il modulo testato chiama un metodo di un oggetto spy che effettua una richiesta HTTP, il test diventa un test di integrazione, non unitario. Regola: se uno spy avvolge un oggetto con operazioni di I/O — non è più un test unitario. Usa i mock per isolare I/O e gli spy solo per oggetti in memoria senza effetti esterni.

Mockito.spy() e spy in MockK su Android

Mockito.spy() — il modo classico per creare una spia in progetti Java/Kotlin. spy() prende un oggetto reale e restituisce un involucro. Tutte le chiamate vengono delegate all’oggetto reale per impostazione predefinita e i risultati vengono registrati. Dopo il test, puoi usare verify() per verificare il numero di chiamate e gli argomenti. Per i metodi che devono restituire dati di test, si usa doReturn/when — questo si chiama “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() — un’alternativa per progetti Kotlin con miglior supporto per coroutine e classi sealed. MockK.spyk() crea una spia, analoga a Mockito.spy(). Supporta coVerify per funzioni suspend e every per lo stubbing parziale. A differenza di Mockito, MockK non supporta le spie per classi final (tutte le classi in Kotlin sono final per impostazione predefinita) — devi aprire la classe (open) o usare un’interfaccia.

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

Stubbing parziale tramite Spy

Stubbing parziale — una tecnica potente ma pericolosa. Puoi creare una spia di un oggetto e sovrascrivere (stub) solo alcuni metodi, lasciando gli altri reali. Esempio: un repository spy dove getUser() restituisce dati di test, mentre saveUser() salva effettivamente in una lista in memoria. Questo permette di combinare i vantaggi degli stub (dati controllati) e delle spie (logica reale). Lo svantaggio: la leggibilità del test ne risente — non è ovvio quali metodi siano reali e quali siano stub.

Implementazione di Spy su iOS con OCMock e protocolli

OCMock per Objective-C — una libreria che supporta la creazione di oggetti spy tramite niceMock. OCMock intercetta le chiamate ai metodi usando il runtime di Objective-C e le registra. Dopo il test viene chiamato verify. OCMock supporta le spie per qualsiasi oggetto (tutti i metodi in Objective-C sono dinamici), il che gli dà un vantaggio su Swift, dove le spie sono possibili solo tramite protocolli.

objective-c
// Creazione di una spia per un oggetto reale
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];

// Esecuzione del test
[spy trackEvent:@"login"];

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

Spy basato su protocollo in Swift — Swift non ha riflessione runtime come Objective-C, quindi le spie vengono create manualmente. Una struttura di test implementa un protocollo e internamente chiama l’oggetto reale mentre registra simultaneamente le chiamate. Ciò richiede più codice, ma è completamente controllabile e type-safe. Le spie manuali non richiedono librerie esterne e non usano runtime — tutto viene verificato in fase di compilazione.

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

Quando usare OCMock vs spy manuale — per codice Objective-C, usa OCMock (meno boilerplate). Per Swift, sono preferibili le spie manuali tramite protocolli. Una spia manuale dà il controllo completo sulla registrazione delle chiamate, non richiede riflessione e funziona con i tipi valore (struct). L’unico svantaggio è che devi mantenere il codice della classe spy sincronizzato con il protocollo quando aggiungi nuovi metodi.

Scenari tipici di utilizzo di Spy

Verifica dell’analitica — il caso d’uso più comune per le spie. Nel codice di produzione, le chiamate di analitica sono sparse in tutta l’applicazione: login, logout, acquisto, errore. Il test crea un involucro spy per AnalyticsTracker, esegue uno scenario (login, visualizzazione prodotto, aggiunta al carrello, acquisto) e verifica che tutti gli eventi necessari siano stati inviati nell’ordine corretto. Un mock non è adatto perché AnalyticsTracker contiene logica di buffering e invio.

Timer e scheduler — testare il codice che usa Handler (Android) o Timer (iOS) è difficile a causa del tempo reale. Un involucro spy per Scheduler registra quali attività sono state pianificate e con quale ritardo. Il test crea una spia del Handler reale, esegue un’azione e verifica che Handler.postDelayed(runnable, delay) sia stato chiamato con il ritardo corretto. L’attività reale non viene eseguita — la spia intercetta e registra la chiamata.

Registrazione e informazioni di debug — in produzione, i log possono essere disabilitati o scritti su file. Un involucro spy per Logger registra tutti i messaggi in una lista in memoria che il test verifica dopo l’esecuzione. Ciò permette di verificare che il messaggio corretto sia stato scritto in caso di errore senza ingombrare la console. Le spie manuali per Logger sono particolarmente utili su iOS, dove OSLog non ha API di test.

Verifica dell’ordine delle chiamate — alcuni scenari richiedono un ordine rigoroso delle operazioni: aprire connessione, inviare dati, chiudere connessione. Mockito permette di verificare l’ordine tramite InOrder.verify(). Una spia fa lo stesso ma preserva l’esecuzione reale. Se sia l’ordine che il risultato di ogni passaggio (la connessione si è effettivamente aperta) sono importanti — usa una spia, non un mock.

Domande frequenti

Spy vs Mock: qual è la differenza principale?

Spy avvolge un oggetto reale ed esegue la sua logica, oltre a registrare le chiamate. Mock sostituisce completamente l’oggetto con uno stub — nessuna logica reale viene eseguita. Una spia preserva il comportamento, un mock no. Scegli una spia quando il lavoro reale dell’oggetto è importante; scegli un mock quando devi isolare il test da una dipendenza esterna.

Quando una Spy è una scelta sbagliata?

Quando un involucro spy porta a operazioni reali di I/O. Se una spia avvolge un oggetto che scrive su file, invia HTTP o legge dal disco — il test non è più unitario. Secondo caso: il test verifica solo il valore restituito senza interessarsi alle chiamate — qui uno stub è sufficiente e una spia è ridondante. Terzo: il codice dipende dallo stato interno della spia — questo è un test fragile.

MockK supporta spy?

Sì, tramite spyk() — l’equivalente di Mockito.spy(). MockK.spyk() crea una spia attorno a un oggetto reale, supporta every per lo stubbing parziale e coVerify/coroutinesVerify per le funzioni suspend. Limitazione: non funziona con classi final (serve open o interfaccia). Per le classi Java, MockK supporta anche spyk() ma richiede l’annotazione @MockKJvmInline.

Si può creare una Spy da un Mock?

Tecnicamente, no. Mock è uno stub che non contiene implementazione reale. Spy, per definizione, avvolge un oggetto reale. In Mockito, non puoi trasformare un mock in spy. Ma puoi fare il contrario: creare una spia e sovrascrivere alcuni metodi tramite doReturn/when (partial mocking). Questo dà un comportamento simile a un mock per i metodi selezionati dell’oggetto spy.

Una Spy in Swift richiede un protocollo?

Sì, è necessario. In Swift, non esiste proxy dinamico come in Java/Kotlin. Per creare una spia, serve un protocollo che sia la classe di produzione che la classe spy implementano. Una spy basata su protocollo in Swift è un’implementazione manuale che prende un oggetto reale, gli delega le chiamate e registra i metadati. Alternativa: la libreria Cuckoo, che genera classi spy tramite SourceKit.

Riepilogo

  • Spy — involucro attorno a un oggetto reale che registra tutte le chiamate ai metodi senza sostituire la logica
  • Differenza da Mock — una spia esegue codice reale, un mock lo sostituisce con uno stub
  • Mockito.spy() — per Java/Kotlin, avvolge un oggetto reale con supporto di verify e partial mock
  • MockK.spyk() — equivalente Kotlin con supporto di coroutine, classi sealed e coVerify
  • iOS — OCMock per Objective-C, spie manuali basate su protocollo per Swift
  • Scenari principali — verifica dell’analitica, timer, log e ordine delle chiamate
  • Attenzione — una spia con operazioni di I/O trasforma un test unitario in un test di integrazione

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche