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 — 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.
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.
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.
| Scenario | Spy | Mock |
|---|---|---|
| Logica reale necessaria | Sì | No (stub) |
| Verifica delle chiamate | Sì (numero, argomenti) | Sì (numero, argomenti) |
| Stubbing parziale | Sì (alcuni metodi spy, altri stub) | No (tutti i metodi sono stub) |
| Rischio di effetti collaterali | Alto (codice reale) | Nullo |
| Velocità | Inferiore (logica reale) | Superiore (stub) |
| Leggibilità | Inferiore (più difficile capire cosa è reale) | Superiore (tutto è esplicito) |
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() — 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”.
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.
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 — 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.
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.
// 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.
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.
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 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 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.
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.
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.
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
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.
Leggi anche