Test Doubles — sunt obiecte de înlocuire utilizate în testele unitare în locul dependențelor reale. Termenul a fost introdus de Gerard Meszaros în cartea „xUnit Test Patterns“ (2007) ca un concept general pentru Mock, Stub, Fake, Spy și Dummy. Potrivit Martin Fowler (2024), Test Doubles permit izolarea componentului testat de mediul său, făcând testele deterministe, rapide și independente de serviciile externe.
Principalele
Test Doubles — este un termen din industria auto (cascador, „dublură“ pentru actori), transferat în dezvoltarea de software. Așa cum un cascador înlocuiește un actor într-o scenă periculoasă, Test Double înlocuiește un component real într-un scenariu de test. Acest lucru este necesar când dependența reală este indisponibilă, lentă, non-deterministă sau are efecte secundare.
Conceptul de Test Double include cinci tipuri specifice, fiecare rezolvând propria sarcină. Tipologia lui Meszaros este canonică și utilizată în toate ghidurile moderne de testare. Diferența între tipuri constă în gradul de control și verificare: de la simpla completare a parametrilor (Dummy) până la verificarea completă a secvenței de apeluri (Mock).
Scopul principal al Test Doubles este izolarea modulului testat. În dezvoltarea mobilă, dependențele reale sunt serverele API, bazele de date, sistemul de fișiere, senzorii dispozitivului, serviciile de sistem (LocationManager, Camera, Bluetooth). Utilizarea directă a acestor componente face testele lente, fragile și dependente de mediu. Potrivit Google Testing Blog (2023), testele unitare bine izolate se execută în milisecunde, iar cele de integrare — în secunde și minute.
Clasificarea Gerard Meszaros include cinci tipuri de Test Doubles, care diferă prin comportament și scopul utilizării. Înțelegerea diferențelor dintre ele stă la baza testării unitare corecte.
Dummy — este un obiect transmis metodei testate, dar care nu este niciodată utilizat. Dummy este necesar doar pentru a satisface semnătura metodei. În Kotlin, acesta este adesea null, emptyList() sau un obiect cu înlocuitori. Dummy nu trebuie să conțină nicio logică — dacă este apelat, testul trebuie să eșueze.
Fake — este o implementare simplificată, dar funcțională a unei interfețe. Spre deosebire de Mock și Stub, Fake conține logică de afaceri reală, dar într-o formă simplificată. Exemplul clasic — InMemoryUserRepository, care stochează date în HashMap în locul unei baze de date. Fake este utilizat când trebuie testată logica dependentă de stare, dar fără costurile infrastructurii reale.
| Tip | Scop | Exemplu |
|---|---|---|
| Dummy | Completarea parametrului | null, obiect gol |
| Fake | Implementare simplificată funcțională | InMemoryRepository |
| Stub | Returnarea unei valori fixe | when(api.getUser()).thenReturn(user) |
| Spy | Înregistrarea apelurilor pentru verificare | verify(spy).save(user) |
| Mock | Verificarea interacțiunii | verify(mock).sendEmail(email) |
Stub returnează valori prestabilite pentru anumite apeluri. Stub nu verifică dacă a fost apelat — pur și simplu furnizează date. În Mockito, Stub se creează prin when(method).thenReturn(value). Stub este ideal pentru testare când dependența trebuie să returneze o valoare specifică, dar simplul fapt al apelului nu este important.
Spy — este un înveliș în jurul unui obiect real care înregistrează toate apelurile pentru verificare ulterioară. Spre deosebire de Mock, Spy delegă apelurile către obiectul real, dar permite verificarea faptului că acestea au avut loc. În Mockito, Spy se creează prin spy(realObject). Spy este util pentru mocking parțial, când se dorește utilizarea unui obiect real, dar verificarea unor apeluri.
Mock — este un obiect cu așteptări de apeluri predefinite. Mock verifică dacă anumite metode au fost apelate cu anumite argumente și într-o anumită ordine. Spre deosebire de Stub, Mock se concentrează pe verificarea comportamentului, nu pe returnarea datelor. Mock este cel mai puternic și cel mai frecvent utilizat tip de Test Double în dezvoltarea mobilă.
Diferența dintre Mock și Stub cauzează adesea confuzie chiar și printre dezvoltatorii experimentați. Diferența principală constă în scop: Stub verifică starea (state verification), Mock verifică comportamentul (behavior verification).
Stub răspunde la întrebarea: „codul a returnat rezultatul corect?“. Mock răspunde la întrebarea: „codul a apelat metodele corecte cu argumentele corecte?“. În dezvoltarea mobilă, Stub este utilizat când rezultatul este important (de exemplu, datele din repository), iar Mock — când efectele secundare sunt importante (de exemplu, trimiterea e-mailului, scrierea în baza de date).
// Stub: verificarea stării
every { repository.getUsers() } returns listOf(user)
val result = useCase.getUsers()
assertEquals(1, result.size)
// Mock: verificarea comportamentului
every { analytics.logEvent("purchase") } returns Unit
useCase.purchase(item)
verify { analytics.logEvent("purchase") }
Exemple practice ale tuturor celor cinci tipuri de Test Doubles în Kotlin utilizând MockK — cea mai populară bibliotecă de mocking pentru proiecte 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: returnăm un răspuns API fix
coEvery { api.register("test@test.com") } returns AuthResult.Success("token123")
val result = useCase.execute("test@test.com")
// Verify: verificăm că utilizatorul a fost salvat
verify { repo.save(any()) }
assertTrue(result.isSuccess())
}
}
data class Logger(val appContext: Context, val format: FormatType)
fun `test logger with dummy context`() {
// Dummy: Context nu este utilizat în interiorul Logger
val dummyContext = mockk<Context>()
val logger = Logger(dummyContext, FormatType.JSON)
assertEquals(FormatType.JSON, logger.format)
}
Alegerea tipului de Test Double depinde de ceea ce se testează exact: starea, comportamentul sau integrarea. În dezvoltarea mobilă pe Android și iOS s-au format următoarele recomandări.
La testarea ViewModel-ului, utilizați Mock pentru dependențele care produc efecte secundare (repository-uri, analitică, navigare) și Stub pentru dependențele care returnează date (clienți API, ContentProvider). Aceasta permite verificarea faptului că ViewModel gestionează corect atât scenariile de succes, cât și pe cele de eroare.
La nivelul Repository, sunt preferate Fake (implementări de bază de date in-memory) și Stub (răspunsuri API fixe). Fake permite verificarea logicii de cache și a modului offline fără configurarea SQLite. Stub simulează diverse coduri HTTP: 200, 404, 500, timeout.
Utilizarea incorectă a Test Doubles — una dintre cele mai frecvente cauze ale testelor fragile care se strică la fiecare refactorizare.
Cea mai frecventă greșeală — mock-area oricărui lucru. Dacă fiecare dependență într-un test este înlocuită cu Mock, testul încetează să verifice comportamentul real. Mock trebuie să fie doar pentru dependențe externe (rețea, bază de date, sistem de fișiere, servicii de sistem). Componentele interne ale aplicației (Value Object, data class, utilitare simple) nu trebuie înlocuite.
A doua greșeală — crearea unui Mock fără definirea așteptărilor. Dacă o metodă este apelată fără every / when, Mock returnează o valoare implicită (null, 0, false). Aceasta poate duce la teste fals-pozitive atunci când Mock returnează în tăcere null, iar testul interpretează acest lucru ca un comportament corect.
A treia greșeală — verificarea fiecărui apel al fiecărui Mock. Verify trebuie utilizat doar pentru apelurile critice din punctul de vedere al logicii de afaceri. Verificarea excesivă face testele fragile: modificarea ordinii apelurilor în codul de producție strică testele fără a modifica comportamentul.
Întrebări frecvente
Stub returnează date și verifică starea (ce s-a returnat), iar Mock verifică comportamentul (ce metode au fost apelate). Stub = „return X“, Mock = „verifică că Y a fost apelat cu argumentul Z“. În testele reale, un singur obiect acționează adesea atât ca Stub, cât și ca Mock simultan.
Fake este preferat în locul Mock-ului atunci când se testează logica dependentă de stare: cache, modul offline, tranzacții. Fake (implementarea in-memory) permite verificarea acestor scenarii fără apeluri fragile de verify. Mock este mai potrivit pentru verificarea trimiterii datelor: analitică, notificări push, e-mail.
Pentru proiectele Android în Kotlin este recomandat MockK. Acesta suportă corutine, funcții suspend, sealed class și funcții extension fără configurare suplimentară. Pentru proiectele în Java, standardul rămâne Mockito — cea mai populară bibliotecă cu documentație extinsă.
Pentru testarea Kotlin Flow, utilizați biblioteca Turbine în combinație cu MockK. Turbine simplifică verificarea emisiei Flow: se poate verifica ordinea valorilor, finalizarea fluxului și excepțiile. Stub pentru Flow returnează flowOf(value), Mock verifică dacă Flow a fost colectat.
Da, dar la nivelul răspunsurilor API, nu al componentelor UI. Bibliotecile MockWebServer (OkHttp) și WireMock permit înlocuirea răspunsurilor HTTP în testele UI. Componentele UI în sine (Compose, SwiftUI Views) nu trebuie înlocuite — comportamentul lor se testează prin teste de screenshot și Espresso.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și