Spy (špión) — testovací objekt, který obaluje reálnou instanci a zaznamenává informace o každém volání: které metody byly volány, s jakými argumenty, kolikrát. Na rozdíl od mocku, spy používá reálnou implementaci obaleného objektu — volání procházejí skutečným kódem a spy pouze zaznamenává fakta. Po provedení testu vývojář zkontroluje záznamy špióna: „Byla metoda sendAnalytics volána třikrát?”. Více — v průvodci testováním Androidu.
Hlavní body
Spy — je obal kolem reálného objektu, který zachycuje všechna volání metod a zaznamenává je. Reálná logika objektu se provádí: pokud metoda ukládá data, počítá hodnotu nebo provádí požadavek — vše se děje jako obvykle. Navíc spy zaznamenává metadata: název metody, argumenty, počet volání, čas provedení. Termín je součástí klasifikace Meszaros (2007) a je podrobně popsán v článku Martina Fowlera „Mocks Aren't Stubs”.
Klíčový rozdíl od Mocku — mock zcela nahrazuje objekt testovacím stubem, všechny metody standardně nic nedělají. Spy obaluje existující objekt: všechny metody standardně fungují jako obvykle, ale zároveň jsou zaznamenávány. Tento rozdíl je zásadní: mock izoluje testovaný kód od reality, spy zachovává realitu a umožňuje ji pozorovat. Volba mezi nimi závisí na tom, co je testováno.
Spy — správná volba — pokud testovaný kód upravuje stav reálného objektu a test musí jak zkontrolovat výsledek (stav), tak se ujistit, že volání byla provedena ve správném pořadí. Mock není vhodný, protože neprovádí reálnou implementaci. Stub není vhodný, protože nezaznamenává volání. Spy je jediný test double, který současně zachovává reálnou logiku a poskytuje informace o voláních.
Mock — úplná izolace. Pokud test nemá záviset na implementaci reálného objektu (například databáze nebo síťový klient), použijte mock. Mock zaručuje, že žádné volání nedosáhne reálné komponenty. To je bezpečné a předvídatelné. Nevýhoda: mock neprovádí reálnou logiku, takže pokud testovaný kód spoléhá na návratovou hodnotu — musí být explicitně nakonfigurována pomocí when/stub.
Spy — reálná logika + pozorování. Pokud testovaný kód interaguje s objektem, jehož logika je pro test důležitá, nejen data — použijte spy. Například AnalyticsTracker, který sbírá události a periodicky je odesílá. Test kontroluje, zda byly události přidány do bufferu a zda se po odeslání buffer vyčistil. Mock to nemůže zkontrolovat, protože neprovádí reálnou logiku trackeru.
| Scénář | Spy | Mock |
|---|---|---|
| Reálná logika potřebná | Ano | Ne (stub) |
| Verifikace volání | Ano (počet, argumenty) | Ano (počet, argumenty) |
| Částečné stubbování | Ano (některé metody — spy, jiné — stub) | Ne (všechny metody — stub) |
| Riziko vedlejších účinků | Vysoké (reálný kód) | Nulové |
| Rychlost | Nižší (reálná logika) | Vyšší (stuby) |
| Čitelnost | Nižší (hůře se chápe, co je reálné) | Vyšší (vše explicitní) |
Spy na všechno — používat spy místo mocku pro všechny testy je chyba. Spy provádí reálný kód, který může mít vedlejší účinky: zápis do souboru, odesílání HTTP, změna globálního stavu. Pokud testovaný modul zavolá metodu spy-objektu, která provádí HTTP požadavek, test se stává integračním testem, nikoli unit testem. Pravidlo: pokud spy obaluje objekt s I/O operacemi — to již není unit test. Použijte mock pro izolaci I/O, spy — pouze pro in-memory objekty bez vnějších účinků.
Mockito.spy() — klasický způsob vytvoření špióna v Java/Kotlin projektech. spy() přijímá reálný objekt a vrací obal. Všechna volání jsou standardně delegována na reálný objekt a výsledky jsou zaznamenávány. Po provedení testu lze pomocí verify() zkontrolovat počet volání a argumenty. Pro metody, které mají vracet testovací data, se používá doReturn/when — nazývá se to „částečné stubbování” (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() — alternativa pro Kotlin projekty s lepší podporou pro korutiny a sealed třídy. MockK.spyk() vytváří špióna, analogicky k Mockito.spy(). Podporuje coVerify pro suspend funkce a every pro částečné stubbování. Na rozdíl od Mockita, MockK nepodporuje spy pro final třídy (všechny třídy v Kotlinu jsou standardně final) — je třeba třídu otevřít (open) nebo použít rozhraní.
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)
}
}
Částečné stubbování — mocná, ale nebezpečná technika. Můžete vytvořit spy objektu a přepsat (stub) pouze některé metody, zatímco ostatní ponechat reálné. Příklad: spy-repozitář, jehož getUser() vrací testovací data a saveUser() skutečně ukládá do in-memory seznamu. To umožňuje kombinovat výhody stubů (kontrolovaná data) a spy (reálná logika). Nevýhoda: složitost čtení testu — není zřejmé, které metody jsou reálné a které jsou stub.
OCMock pro Objective-C — knihovna podporující vytváření spy-objektů přes niceMock. OCMock zachycuje volání metod pomocí Objective-C runtime a zaznamenává je. Po provedení testu je volána verify. OCMock podporuje spy pro libovolný objekt (v Objective-C jsou všechny metody dynamické), což poskytuje výhodu oproti Swiftu, kde je spy možný pouze přes protokoly.
// Vytvoření spy pro reálný objekt
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];
// Provedení testu
[spy trackEvent:@"login"];
// Verifikace
[[spy verify] trackEvent:@"login"];
XCTAssertEqual([realTracker eventCount], 1);
Swift protocol-based spy — ve Swiftu není Objective-C runtime reflexe, proto se spy vytváří ručně. Testovací struktura implementuje protokol a interně volá reálný objekt, současně zaznamenávajíc volání. To je více kódu, ale plně kontrolovatelné a typově bezpečné. Ruční špióni nevyžadují externí knihovny a nepoužívají runtime — vše je kontrolováno ve fázi kompilace.
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)
}
}
Kdy použít OCMock vs ruční spy — pro Objective-C kód použijte OCMock (méně boilerplate). Pro Swift — preferováni jsou ruční špióni přes protokoly. Ruční spy poskytuje plnou kontrolu nad zaznamenáváním volání, nevyžaduje reflexi a pracuje s hodnotovými typy (struct). Jedinou nevýhodou je nutnost udržovat kód spy třídy synchronizovaný s protokolem při přidávání nových metod.
Kontrola analytiky — nejčastější scénář použití spy. V produkčním kódu jsou volání analytiky rozptýlena po celé aplikaci: login, logout, purchase, error. Test vytvoří spy-obal pro AnalyticsTracker, provede scénář (přihlášení, prohlížení produktu, přidání do košíku, nákup) a zkontroluje, zda byly všechny potřebné události odeslány ve správném pořadí. Mock není vhodný, protože AnalyticsTracker obsahuje logiku bufferování a odesílání.
Časovače a plánovače — testování kódu používajícího Handler (Android) nebo Timer (iOS) je kvůli reálnému času obtížné. Spy-obal pro Scheduler zaznamenává, které úkoly byly naplánovány a s jakým zpožděním. Test vytvoří spy reálného Handleru, provede akci a zkontroluje, zda byl Handler.postDelayed(runnable, delay) volán se správným zpožděním. Reálný úkol se přitom neprovádí — spy zachycuje a zaznamenává volání.
Logování a debug informace — v produkci mohou být logy vypnuté nebo zapisované do souboru. Spy-obal pro Logger zaznamenává všechny zprávy do in-memory seznamu, který test po provedení zkontroluje. To umožňuje zkontrolovat, zda se při chybě zapisuje správná zpráva, bez zaneřádění konzole. Ruční špióni pro Logger jsou zvláště užiteční na iOS, kde OSLog nemá testovací API.
Kontrola pořadí volání — některé scénáře vyžadují striktní pořadí operací: otevřít spojení, odeslat data, zavřít spojení. Mockito umožňuje kontrolu pořadí pomocí InOrder.verify(). Spy dělá totéž, ale zachovává reálné provedení. Pokud je důležité nejen pořadí, ale také výsledek každého kroku (spojení se skutečně otevřelo) — použijte spy, ne mock.
Často kladené otázky
Spy obaluje reálný objekt a provádí jeho logiku, navíc zaznamenávajíc volání. Mock zcela nahrazuje objekt stubem — neprovádí se žádná reálná logika. Spy zachovává chování, mock ne. Vyberte spy, když je důležitá reálná práce objektu; vyberte mock, když je třeba test izolovat od externí závislosti.
Když spy-obal vede k reálným I/O operacím. Pokud spy obaluje objekt, který zapisuje do souboru, odesílá HTTP nebo čte z disku — test přestává být unit testem. Druhý případ: test kontroluje pouze návratovou hodnotu bez zájmu o volání — zde stačí stub a spy je nadbytečný. Třetí: kód spoléhá na vnitřní stav spy — to je křehký test.
Ano, pomocí spyk() — analogicky k Mockito.spy(). MockK.spyk() vytváří špióna kolem reálného objektu, podporuje every pro částečné stubbování a coVerify/coroutinesVerify pro suspend funkce. Omezení: nefunguje s final třídami (je potřeba open nebo rozhraní). Pro Java třídy MockK také podporuje spyk(), ale vyžaduje anotaci @MockKJvmInline.
Technicky — ne. Mock je stub, který neobsahuje reálnou implementaci. Spy podle definice obaluje reálný objekt. V Mockitu nelze mock přeměnit na spy. Ale opak je možný: vytvořte spy a přepište část metod pomocí doReturn/when (partial mocking). To poskytuje chování podobné mocku pro vybrané metody spy-objektu.
Nutně. Ve Swiftu neexistuje dynamické proxy jako v Java/Kotlin. Pro vytvoření spy je potřeba protokol, který implementuje jak produkční třída, tak spy třída. Swift-protocol-based spy je ruční implementace, která přijímá reálný objekt, deleguje mu volání a zaznamenává metadata. Alternativa: knihovna Cuckoo, která generuje spy třídy pomocí SourceKit.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také