Spy (spion) — un obiect de test care înfășoară o instanță reală și înregistrează informații despre fiecare apel: ce metode au fost apelate, cu ce argumente, de câte ori. Spre deosebire de mock, spy folosește implementarea reală a obiectului înfășurat — apelurile trec prin codul real, iar spy doar înregistrează faptele. După executarea testului, dezvoltatorul verifică înregistrările spionului: „Metoda sendAnalytics a fost apelată de trei ori?”. Mai multe — în ghidul Android de testare.
Principalele puncte
Spy — este o înfășurare în jurul unui obiect real care interceptează toate apelurile metodelor și le înregistrează. Logica reală a obiectului este executată: dacă metoda salvează date, calculează o valoare sau face o cerere — totul se întâmplă ca de obicei. În plus, spy înregistrează metadate: numele metodei, argumentele, numărul de apeluri, timpul de execuție. Termenul face parte din clasificarea Meszaros (2007) și este descris detaliat în articolul lui Martin Fowler „Mocks Aren't Stubs”.
Diferența cheie față de Mock — mock înlocuiește complet obiectul cu un stub de test, toate metodele implicit nu fac nimic. Spy înfășoară un obiect existent: toate metodele implicit funcționează ca de obicei, dar în același timp sunt înregistrate. Această diferență este fundamentală: mock izolează codul testat de realitate, spy păstrează realitatea și permite observarea ei. Alegerea între ele depinde de ceea ce este testat.
Spy — alegerea corectă — dacă codul testat modifică starea obiectului real, iar testul trebuie atât să verifice rezultatul (starea), cât și să se asigure că apelurile au fost făcute în ordinea corectă. Mock nu este potrivit pentru că nu execută implementarea reală. Stub nu este potrivit pentru că nu înregistrează apelurile. Spy este singurul test double care păstrează simultan logica reală și oferă informații despre apeluri.
Mock — izolare completă. Dacă testul nu trebuie să depindă de implementarea obiectului real (de exemplu, baza de date sau clientul de rețea), folosiți mock. Mock garantează că niciun apel nu va ajunge la componenta reală. Este sigur și previzibil. Dezavantaj: mock nu execută logica reală, deci dacă codul testat se bazează pe valoarea returnată — trebuie configurat explicit prin when/stub.
Spy — logică reală + observare. Dacă codul testat interacționează cu un obiect a cărui logică este importantă pentru test, nu doar datele — folosiți spy. De exemplu, AnalyticsTracker care colectează evenimente și le trimite periodic. Testul verifică dacă evenimentele au fost adăugate în buffer și după trimitere bufferul s-a golit. Mock nu poate verifica acest lucru pentru că nu execută logica reală a tracker-ului.
| Scenariu | Spy | Mock |
|---|---|---|
| Logica reală necesară | Da | Nu (stub) |
| Verificarea apelurilor | Da (număr, argumente) | Da (număr, argumente) |
| Stub parțial | Da (unele metode — spy, altele — stub) | Nu (toate metodele — stub) |
| Risc de efecte secundare | Ridicat (cod real) | Zero |
| Viteză | Mai mică (logică reală) | Mai mare (stub) |
| Lizibilitate | Mai mică (mai greu de înțeles ce e real) | Mai mare (totul explicit) |
Spy pentru tot — a folosi spy în loc de mock pentru toate testele este o greșeală. Spy execută cod real care poate avea efecte secundare: scriere în fișier, trimitere HTTP, modificare a stării globale. Dacă modulul testat apelează o metodă a obiectului spy care face o cerere HTTP, testul devine unul de integrare, nu un test unitar. Regula: dacă spy înfășoară un obiect cu operații I/O — acesta nu mai este un test unitar. Folosiți mock pentru izolarea I/O, spy — doar pentru obiecte in-memory fără efecte externe.
Mockito.spy() — metoda clasică de creare a unui spion în proiecte Java/Kotlin. spy() primește un obiect real și returnează o înfășurare. Toate apelurile sunt implicit delegate obiectului real, iar rezultatele sunt înregistrate. După executarea testului, prin verify() se poate verifica numărul de apeluri și argumentele. Pentru metodele care trebuie să returneze date de test, se folosește doReturn/when — aceasta se numește „stub parțial” (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() — alternativă pentru proiecte Kotlin cu suport mai bun pentru corutine și clase sealed. MockK.spyk() creează un spion, analog cu Mockito.spy(). Suportă coVerify pentru funcții suspend și every pentru stub parțial. Spre deosebire de Mockito, MockK nu suportă spy pentru clase final (toate clasele în Kotlin sunt final implicit) — trebuie fie să deschideți clasa (open), fie să folosiți o interfață.
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)
}
}
Stub parțial — o tehnică puternică, dar periculoasă. Puteți face spy al unui obiect și să suprascrieți (stub) doar unele metode, lăsându-le pe celelalte reale. Exemplu: un spy-repository al cărui getUser() returnează date de test, iar saveUser() salvează efectiv într-o listă în memorie. Aceasta permite combinarea avantajelor stuburilor (date controlate) și spy (logică reală). Dezavantaj: complexitatea citirii testului — nu este evident ce metode sunt reale și care sunt stub.
OCMock pentru Objective-C — bibliotecă care suportă crearea obiectelor spy prin niceMock. OCMock interceptează apelurile metodelor folosind runtime-ul Objective-C și le înregistrează. După executarea testului se apelează verify. OCMock suportă spy pentru orice obiect (în Objective-C toate metodele sunt dinamice), ceea ce oferă un avantaj față de Swift, unde spy este posibil doar prin protocoale.
// Crearea spy pentru obiectul real
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];
// Executarea testului
[spy trackEvent:@"login"];
// Verificare
[[spy verify] trackEvent:@"login"];
XCTAssertEqual([realTracker eventCount], 1);
Swift protocol-based spy — în Swift nu există reflexie runtime Objective-C, deci spy este creat manual. Structura de test implementează protocolul și intern apelează obiectul real, în același timp înregistrând apelurile. Este mai mult cod, dar complet controlabil și type-safe. Spionii manuali nu necesită biblioteci externe și nu folosesc runtime — totul este verificat în faza de compilare.
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)
}
}
Când să folosiți OCMock vs spy manual — pentru cod Objective-C folosiți OCMock (mai puțin boilerplate). Pentru Swift — sunt preferați spionii manuali prin protocoale. Spy manual oferă control complet asupra înregistrării apelurilor, nu necesită reflexie și funcționează cu tipuri de valoare (struct). Singurul dezavantaj: trebuie să mențineți codul clasei spy sincronizat cu protocolul la adăugarea de noi metode.
Verificarea analiticii — cel mai frecvent scenariu de utilizare a spy. În codul de producție, apelurile de analitică sunt răspândite în întreaga aplicație: login, logout, purchase, error. Testul creează o înfășurare spy pentru AnalyticsTracker, execută scenariul (autentificare, vizualizare produs, adăugare în coș, cumpărare) și verifică că toate evenimentele necesare au fost trimise în ordinea corectă. Mock nu este potrivit pentru că AnalyticsTracker conține logică de bufferizare și trimitere.
Timer-e și planificatoare — testarea codului care folosește Handler (Android) sau Timer (iOS) este dificilă din cauza timpului real. Înfășurarea spy pentru Scheduler înregistrează ce sarcini au fost planificate și cu ce întârziere. Testul creează spy al Handler-ului real, execută acțiunea și verifică dacă Handler.postDelayed(runnable, delay) a fost apelat cu întârzierea corectă. Sarcina reală nu este executată în acest timp — spy interceptează și înregistrează apelul.
Logare și informații de debug — în producție, logurile pot fi dezactivate sau scrise în fișier. Înfășurarea spy pentru Logger înregistrează toate mesajele într-o listă în memorie pe care testul o verifică după execuție. Aceasta permite verificarea că la eroare se scrie un mesaj corect, fără a aglomera consola. Spionii manuali pentru Logger sunt deosebit de utili pe iOS, unde OSLog nu are API de test.
Verificarea ordinii apelurilor — unele scenarii necesită o ordine strictă a operațiilor: deschide conexiunea, trimite date, închide conexiunea. Mockito permite verificarea ordinii prin InOrder.verify(). Spy face același lucru, dar păstrează execuția reală. Dacă este importantă nu doar ordinea, ci și rezultatul fiecărui pas (conexiunea s-a deschis cu adevărat) — folosiți spy, nu mock.
Întrebări frecvente
Spy înfășoară obiectul real și execută logica sa, în plus înregistrând apelurile. Mock înlocuiește complet obiectul cu un stub — nicio logică reală nu este executată. Spy păstrează comportamentul, mock — nu. Alegeți spy când munca reală a obiectului este importantă; mock — când trebuie să izolați testul de o dependență externă.
Când înfășurarea spy duce la operații reale I/O. Dacă spy înfășoară un obiect care scrie în fișier, trimite HTTP sau citește de pe disc — testul încetează să mai fie un test unitar. Al doilea caz: testul verifică doar valoarea returnată fără interes pentru apeluri — aici este suficient stub, iar spy este redundant. Al treilea: codul se bazează pe starea internă a spy — acesta este un test fragil.
Da, prin spyk() — analog cu Mockito.spy(). MockK.spyk() creează un spion în jurul obiectului real, suportă every pentru stub parțial și coVerify/coroutinesVerify pentru funcții suspend. Limitare: nu funcționează cu clase final (necesită open sau interfață). Pentru clase Java, MockK suportă de asemenea spyk(), dar necesită adnotarea @MockKJvmInline.
Tehnic — nu. Mock este un stub care nu conține o implementare reală. Spy prin definiție înfășoară un obiect real. În Mockito nu se poate transforma un mock în spy. Dar se poate face invers: creați un spy și suprascrieți o parte din metode prin doReturn/when (partial mocking). Aceasta oferă un comportament similar cu mock pentru metodele selectate ale obiectului spy.
Obligatoriu. În Swift nu există proxy dinamic ca în Java/Kotlin. Pentru crearea unui spy este nevoie de un protocol pe care îl implementează atât clasa de producție, cât și clasa spy. Swift-protocol-based spy este o implementare manuală care primește obiectul real, îi delegă apelurile și înregistrează metadatele. Alternativă: biblioteca Cuckoo, care generează clase spy prin SourceKit.
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