I Test Doubles sono oggetti sostituti utilizzati nei test unitari al posto delle dipendenze reali. Il termine è stato introdotto da Gerard Meszaros nel libro “xUnit Test Patterns” (2007) come concetto generale per Mock, Stub, Fake, Spy e Dummy. Secondo Martin Fowler (2024), i Test Doubles consentono di isolare il componente sotto test dal suo ambiente, rendendo i test deterministici, veloci e indipendenti dai servizi esterni.
Punti chiave
Test Doubles è un termine dell'industria automobilistica (controfigura), trasferito nello sviluppo software. Come una controfigura sostituisce un attore in una scena pericolosa, un Test Double sostituisce un componente reale in uno scenario di test. Ciò è necessario quando la dipendenza reale non è disponibile, è lenta, non deterministica o ha effetti collaterali.
Il concetto di Test Double comprende cinque tipi specifici, ognuno dei quali risolve il proprio compito. La tipologia di Meszaros è canonica e utilizzata in tutte le guide moderne sui test. La differenza tra i tipi risiede nel grado di controllo e verifica: dal semplice riempimento di parametri (Dummy) alla verifica completa della sequenza di chiamate (Mock).
Lo scopo principale dei Test Doubles è isolare il modulo sotto test. Nello sviluppo mobile, le dipendenze reali includono server API, database, file system, sensori del dispositivo e servizi di sistema (LocationManager, Camera, Bluetooth). L'uso diretto di questi componenti rende i test lenti, fragili e dipendenti dall'ambiente. Secondo Google Testing Blog (2023), i test unitari ben isolati vengono eseguiti in millisecondi, mentre i test di integrazione vengono eseguiti in secondi e minuti.
La classificazione di Gerard Meszaros include cinque tipi di Test Doubles, che differiscono per comportamento e scopo. Comprendere la differenza tra loro è la base per test unitari competenti.
Dummy è un oggetto che viene passato al metodo sotto test ma non viene mai utilizzato. Dummy è necessario solo per soddisfare la firma del metodo. In Kotlin, questo è spesso null, emptyList() o un oggetto con stub. Dummy non deve contenere alcuna logica — se viene chiamato, il test deve fallire.
Fake è un'implementazione semplificata ma funzionante di un'interfaccia. A differenza di Mock e Stub, Fake contiene logica di business reale, ma in forma semplificata. Un esempio classico è InMemoryUserRepository, che memorizza i dati in una HashMap invece di un database. Fake viene utilizzato quando è necessario testare logica che dipende dallo stato, ma senza il sovraccarico dell'infrastruttura reale.
| Tipo | Scopo | Esempio |
|---|---|---|
| Dummy | Riempire un parametro | null, oggetto vuoto |
| Fake | Implementazione semplificata funzionante | InMemoryRepository |
| Stub | Restituire un valore fisso | when(api.getUser()).thenReturn(user) |
| Spy | Registrare chiamate per verifica | verify(spy).save(user) |
| Mock | Verificare l'interazione | verify(mock).sendEmail(email) |
Stub restituisce valori predefiniti per chiamate specifiche. Stub non verifica se è stato chiamato — fornisce semplicemente dati. In Mockito, Stub viene creato tramite when(method).thenReturn(value). Stub è ideale per test quando è necessario che una dipendenza restituisca un valore specifico, ma il fatto stesso della chiamata non è importante.
Spy è un involucro attorno a un oggetto reale che registra tutte le chiamate per la verifica successiva. A differenza di Mock, Spy delega le chiamate all'oggetto reale ma consente di verificare che siano avvenute. In Mockito, Spy viene creato tramite spy(realObject). Spy è utile per il mocking parziale, quando si desidera utilizzare un oggetto reale ma verificare alcune chiamate.
Mock è un oggetto con aspettative di chiamata predefinite. Mock verifica che metodi specifici siano stati chiamati con argomenti specifici e in un ordine specifico. A differenza di Stub, Mock si concentra sulla verifica del comportamento piuttosto che sulla restituzione dei dati. Mock è il tipo di Test Double più potente e più utilizzato nello sviluppo mobile.
La differenza tra Mock e Stub causa spesso confusione anche tra sviluppatori esperti. La differenza principale risiede nello scopo: Stub verifica lo stato (state verification), Mock verifica il comportamento (behavior verification).
Stub risponde alla domanda: “il codice ha restituito il risultato corretto?”. Mock risponde alla domanda: “il codice ha chiamato i metodi corretti con gli argomenti corretti?”. Nello sviluppo mobile, Stub viene utilizzato quando il risultato è importante (es., dati da un repository), mentre Mock viene utilizzato quando gli effetti collaterali sono importanti (es., inviare email, scrivere su un database).
// Stub: verifica dello stato
every { repository.getUsers() } returns listOf(user)
val result = useCase.getUsers()
assertEquals(1, result.size)
// Mock: verifica del comportamento
every { analytics.logEvent("purchase") } returns Unit
useCase.purchase(item)
verify { analytics.logEvent("purchase") }
Esempi pratici di tutti e cinque i tipi di Test Doubles in Kotlin utilizzando MockK — la libreria di mocking più popolare per progetti Android.
class InMemoryUserRepository : UserRepository {
private val store = mutableMapOf<String, User>()
override fun save(user: User) {
store[user.email] = user
}
override fun findByEmail(email: String): User? {
return store[email]
}
}
class RegisterUseCaseTest {
private val api = mockk<AuthApi>()
private val repo = spyk(InMemoryUserRepository())
private val useCase = RegisterUseCase(api, repo)
fun `register user successfully`() = runTest {
// Stub: restituire risposta API fissa
coEvery { api.register("test@test.com") } returns AuthResult.Success("token123")
val result = useCase.execute("test@test.com")
// Verify: verificare che l'utente sia stato salvato
verify { repo.save(any()) }
assertTrue(result.isSuccess())
}
}
data class Logger(val appContext: Context, val format: FormatType)
fun `test logger with dummy context`() {
// Dummy: Context non viene utilizzato all'interno di Logger
val dummyContext = mockk<Context>()
val logger = Logger(dummyContext, FormatType.JSON)
assertEquals(FormatType.JSON, logger.format)
}
La scelta del tipo di Test Double dipende da cosa viene esattamente testato: stato, comportamento o integrazione. Nello sviluppo mobile Android e iOS, sono state stabilite le seguenti raccomandazioni.
Quando si testa ViewModel, utilizzare Mock per le dipendenze che producono effetti collaterali (repository, analytics, navigazione) e Stub per le dipendenze che restituiscono dati (client API, ContentProvider). Ciò consente di verificare che il ViewModel gestisca correttamente sia gli scenari di successo che quelli di errore.
A livello di Repository, preferire Fake (implementazioni di database in memoria) e Stub (risposte API fisse). Fake consente di testare la logica di caching e la modalità offline senza configurare SQLite. Stub simula vari stati HTTP: 200, 404, 500, timeout.
L'uso scorretto dei Test Doubles è una delle cause più comuni di test fragili che si rompono a ogni refactoring.
L'errore più comune è mockare tutto. Se ogni dipendenza in un test viene sostituita con un Mock, il test smette di verificare il comportamento reale. Mock dovrebbe essere usato solo per dipendenze esterne (rete, database, file system, servizi di sistema). I componenti interni dell'applicazione (Value Object, data class, semplici utility) non dovrebbero essere sostituiti.
Il secondo errore è creare un Mock senza definire le aspettative. Se un metodo viene chiamato senza every / when, Mock restituisce un valore predefinito (null, 0, false). Ciò può portare a test falso-positivi, in cui Mock restituisce silenziosamente null e il test interpreta questo come comportamento corretto.
Il terzo errore è verificare ogni chiamata di ogni Mock. Verify dovrebbe essere usato solo per chiamate che sono criticamente importanti dal punto di vista della logica di business. La verifica eccessiva rende i test fragili: cambiare l'ordine delle chiamate nel codice di produzione rompe i test senza modificare il comportamento.
Domande frequenti
Stub restituisce dati e verifica lo stato (cosa è stato restituito), mentre Mock verifica il comportamento (quali metodi sono stati chiamati). Stub = “restituisci X”, Mock = “verifica che Y sia stato chiamato con argomento Z”. Nei test reali, un oggetto spesso funge contemporaneamente da Stub e Mock.
Fake è preferibile a Mock quando si testa logica che dipende dallo stato: caching, modalità offline, transazioni. Fake (implementazione in memoria) consente di testare questi scenari senza fragili chiamate verify. Mock è più adatto per verificare l'invio di dati: analytics, push, email.
Per progetti Android in Kotlin, si consiglia MockK. Supporta coroutine, funzioni suspend, classi sealed e funzioni di estensione senza configurazione aggiuntiva. Per progetti Java, Mockito rimane lo standard — la libreria più popolare con documentazione estesa.
Per testare Kotlin Flow, utilizzare la libreria Turbine insieme a MockK. Turbine semplifica la verifica delle emissioni di Flow: è possibile verificare l'ordine dei valori, il completamento del flusso e le eccezioni. Stub per Flow restituisce flowOf(value), Mock verifica che il Flow sia stato raccolto.
Sì, ma a livello di risposte API, non di componenti UI. Le librerie MockWebServer (OkHttp) e WireMock consentono di simulare risposte HTTP nei test UI. I componenti UI stessi (Compose, SwiftUI Views) non dovrebbero essere sostituiti — il loro comportamento viene testato tramite screenshot test ed Espresso.
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