Spy (kém) — egy tesztobjektum, amely becsomagol egy valós példányt és rögzíti az információkat minden hívásról: mely metódusok kerültek meghívásra, milyen argumentumokkal, hányszor. Ellentétben a mock-kal, a spy a becsomagolt objektum valós implementációját használja — a hívások a valódi kódon mennek keresztül, a spy pedig csak rögzíti a tényeket. A teszt végrehajtása után a fejlesztő ellenőrzi a kém feljegyzéseit: „Háromszor hívták meg a sendAnalytics metódust?”. Bővebben — az Android tesztelési útmutatóban.
Főbb pontok
Spy — egy burkolat egy valós objektum körül, amely elfogja az összes metódushívást és rögzíti azokat. Az objektum valós logikája végrehajtódik: ha a metódus adatokat ment, értéket számol ki vagy kérést intéz — minden a szokásos módon történik. Ezenkívül a spy metaadatokat rögzít: a metódus nevét, argumentumokat, hívások számát, végrehajtási időt. A kifejezés Meszaros (2007) osztályozásának része, és Martin Fowler „Mocks Aren't Stubs” című cikkében részletesen le van írva.
Fő különbség a Mock-tól — a mock teljesen helyettesíti az objektumot egy teszt-stub-bal, minden metódus alapértelmezés szerint nem csinál semmit. A spy egy meglévő objektumot csomagol be: minden metódus alapértelmezés szerint a szokásos módon működik, de közben rögzítésre kerül. Ez a különbség alapvető: a mock elkülöníti a tesztelt kódot a valóságtól, a spy megőrzi a valóságot és lehetővé teszi annak megfigyelését. A választás attól függ, hogy mit tesztelünk.
Spy — a helyes választás — ha a tesztelt kód módosítja egy valós objektum állapotát, és a tesztnek egyaránt ellenőriznie kell az eredményt (állapotot) és meg kell győződnie arról, hogy a hívások a helyes sorrendben történtek. A mock nem megfelelő, mert nem hajtja végre a valós implementációt. A stub nem megfelelő, mert nem rögzíti a hívásokat. A spy az egyetlen tesztduplum, amely egyszerre őrzi meg a valós logikát és nyújt információt a hívásokról.
Mock — teljes elkülönítés. Ha a teszt nem függhet a valós objektum implementációjától (például adatbázis vagy hálózati kliens), használjon mock-ot. A mock garantálja, hogy egyetlen hívás sem éri el a valós komponenst. Ez biztonságos és kiszámítható. Hátrány: a mock nem hajt végre valós logikát, így ha a tesztelt kód a visszatérési értékre támaszkodik — azt explicit módon kell konfigurálni when/stub segítségével.
Spy — valós logika + megfigyelés. Ha a tesztelt kód olyan objektummal lép kölcsönhatásba, amelynek logikája fontos a teszt szempontjából, nem csak az adatok — használjon spy-t. Például AnalyticsTracker, amely eseményeket gyűjt és időszakonként elküldi azokat. A teszt ellenőrzi, hogy az események hozzáadásra kerültek-e a pufferhez, és hogy a küldés után a puffer kiürült-e. A mock nem tudja ezt ellenőrizni, mert nem hajtja végre a tracker valós logikáját.
| Forgatókönyv | Spy | Mock |
|---|---|---|
| Valós logika szükséges | Igen | Nem (stub) |
| Hívások ellenőrzése | Igen (szám, argumentumok) | Igen (szám, argumentumok) |
| Részleges stubbólás | Igen (egyes metódusok — spy, mások — stub) | Nem (minden metódus — stub) |
| Mellékhatások kockázata | Magas (valós kód) | Nulla |
| Sebesség | Alacsonyabb (valós logika) | Magasabb (stub-ok) |
| Olvashatóság | Alacsonyabb (nehezebb megérteni, mi valós) | Magasabb (minden explicit) |
Spy mindenhez — a spy használata mock helyett minden tesztben hiba. A spy valós kódot hajt végre, amelynek mellékhatásai lehetnek: fájlba írás, HTTP küldés, globális állapot megváltoztatása. Ha a tesztelt modul meghív egy spy-objektum metódusát, amely HTTP-kérést intéz, a teszt integrációs tesztté válik, nem pedig egységtesztté. Szabály: ha a spy I/O műveletekkel rendelkező objektumot csomagol be — ez már nem egységteszt. Használjon mock-ot az I/O elkülönítésére, spy-t — csak memóriabeli objektumokhoz külső hatások nélkül.
Mockito.spy() — a klasszikus módja a kém létrehozásának Java/Kotlin projektekben. A spy() átvesz egy valós objektumot és visszaad egy burkolatot. Minden hívás alapértelmezés szerint a valós objektumra delegálódik, az eredmények pedig rögzítésre kerülnek. A teszt végrehajtása után a verify() segítségével ellenőrizhető a hívások száma és az argumentumok. Azoknál a metódusoknál, amelyeknek tesztadatokat kell visszaadniuk, a doReturn/when használatos — ezt nevezzük „részleges stubbólásnak” (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() — alternatíva Kotlin projektekhez, jobb támogatással a korutinokhoz és sealed osztályokhoz. A MockK.spyk() kémet hoz létre, a Mockito.spy() analógja. Támogatja a coVerify-t a suspend függvényekhez és az every-t a részleges stubbóláshoz. Ellentétben a Mockito-val, a MockK nem támogatja a spy-t final osztályokhoz (Kotlinban minden osztály alapértelmezés szerint final) — az osztályt meg kell nyitni (open) vagy interfészt kell használni.
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)
}
}
Részleges stubbólás — egy erőteljes, de veszélyes technika. Létrehozhat egy objektum spy-ját, és csak néhány metódust írhat felül (stub), a többit valósként hagyva. Példa: egy spy-adattár, amelynek getUser() tesztadatokat ad vissza, a saveUser() pedig ténylegesen elment egy memóriabeli listába. Ez lehetővé teszi a stub-ok (ellenőrzött adatok) és a spy (valós logika) előnyeinek kombinálását. Hátrány: a teszt olvasásának bonyolultsága — nem nyilvánvaló, mely metódusok valósak és melyek stub-ok.
OCMock Objective-C-hez — könyvtár, amely támogatja spy-objektumok létrehozását niceMock segítségével. Az OCMock elfogja a metódushívásokat az Objective-C runtime segítségével és rögzíti azokat. A teszt végrehajtása után a verify meghívásra kerül. Az OCMock támogatja a spy-t bármely objektumhoz (Objective-C-ben minden metódus dinamikus), ami előnyt jelent Swift-hez képest, ahol a spy csak protokollokon keresztül lehetséges.
// Spy létrehozása a valós objektumhoz
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];
// Teszt végrehajtása
[spy trackEvent:@"login"];
// Ellenőrzés
[[spy verify] trackEvent:@"login"];
XCTAssertEqual([realTracker eventCount], 1);
Swift protocol-based spy — Swift-ben nincs Objective-C runtime-reflexió, ezért a spy manuálisan jön létre. A tesztstruktúra implementálja a protokollt és belül meghívja a valós objektumot, miközben rögzíti a hívásokat. Ez több kód, de teljesen kontrollálható és típusbiztos. A manuális kémek nem igényelnek külső könyvtárakat és nem használnak runtime-ot — minden a fordítási fázisban ellenőrzésre kerül.
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)
}
}
Mikor használjunk OCMock-ot vs manuális spy-t — Objective-C kódhoz használjon OCMock-ot (kevesebb boilerplate). Swift-hez — a manuális kémek protokollokon keresztül előnyösebbek. A manuális spy teljes ellenőrzést biztosít a hívások rögzítése felett, nem igényel reflexiót, és értéktípusokkal (struct) működik. Az egyetlen hátrány: a spy-osztály kódját szinkronban kell tartani a protokollal új metódusok hozzáadásakor.
Analitika ellenőrzése — a spy használatának leggyakoribb forgatókönyve. A termelési kódban az analitikai hívások szétszórva vannak az egész alkalmazásban: login, logout, purchase, error. A teszt létrehoz egy spy-burkolatot az AnalyticsTracker számára, végrehajtja a forgatókönyvet (bejelentkezés, termék megtekintése, kosárba helyezés, vásárlás) és ellenőrzi, hogy az összes szükséges esemény a helyes sorrendben került-e elküldésre. A mock nem megfelelő, mert az AnalyticsTracker pufferelési és küldési logikát tartalmaz.
Időzítők és ütemezők — a Handler-t (Android) vagy Timer-t (iOS) használó kód tesztelése a valós idő miatt nehéz. A Scheduler spy-burkolata rögzíti, hogy mely feladatok lettek ütemezve és milyen késleltetéssel. A teszt létrehozza a valós Handler spy-ját, végrehajtja a műveletet és ellenőrzi, hogy a Handler.postDelayed(runnable, delay) a megfelelő késleltetéssel lett-e meghívva. A valós feladat ezalatt nem hajtódik végre — a spy elfogja és rögzíti a hívást.
Naplózás és hibakeresési információ — termelésben a naplók lehetnek kikapcsolva vagy fájlba írva. A Logger spy-burkolata az összes üzenetet egy memóriabeli listába rögzíti, amelyet a teszt a végrehajtás után ellenőriz. Ez lehetővé teszi annak ellenőrzését, hogy hiba esetén a megfelelő üzenet kerül-e kiírásra anélkül, hogy a konzol eltömődne. A manuális kémek a Logger számára különösen hasznosak iOS-en, ahol az OSLog-nak nincs teszt API-ja.
Hívások sorrendjének ellenőrzése — egyes forgatókönyvek szigorú műveleti sorrendet igényelnek: kapcsolat megnyitása, adatok küldése, kapcsolat bezárása. A Mockito lehetővé teszi a sorrend ellenőrzését az InOrder.verify() segítségével. A spy ugyanezt teszi, de megőrzi a valós végrehajtást. Ha nem csak a sorrend, hanem minden lépés eredménye is fontos (a kapcsolat ténylegesen megnyílt) — használjon spy-t, ne mock-ot.
Gyakran Ismételt Kérdések
Spy becsomagol egy valós objektumot és végrehajtja a logikáját, ezenkívül rögzíti a hívásokat. A mock teljesen helyettesíti az objektumot egy stub-bal — semmilyen valós logika nem hajtódik végre. A spy megőrzi a viselkedést, a mock nem. Válassza a spy-t, ha az objektum valós működése fontos; válassza a mock-ot, ha a tesztet el kell különíteni egy külső függőségtől.
Amikor a spy-burkolat valós I/O műveletekhez vezet. Ha a spy olyan objektumot csomagol be, amely fájlba ír, HTTP-t küld vagy lemezről olvas — a teszt megszűnik egységteszt lenni. Második eset: a teszt csak a visszatérési értéket ellenőrzi a hívások iránti érdeklődés nélkül — itt a stub elegendő, a spy felesleges. Harmadik: a kód a spy belső állapotára támaszkodik — ez egy törékeny teszt.
Igen, a spyk() segítségével — a Mockito.spy() analógja. A MockK.spyk() kémet hoz létre egy valós objektum körül, támogatja az every-t a részleges stubbóláshoz és a coVerify/coroutinesVerify-t a suspend függvényekhez. Korlátozás: nem működik final osztályokkal (open vagy interfész szükséges). Java osztályokhoz a MockK szintén támogatja a spyk()-t, de megköveteli a @MockKJvmInline annotációt.
Technikailag — nem. A Mock egy stub, amely nem tartalmaz valós implementációt. A spy definíció szerint egy valós objektumot csomagol be. A Mockito-ban nem lehet a mock-ot spy-vá alakítani. De fordítva lehetséges: hozzon létre egy spy-t, és írja felül a metódusok egy részét a doReturn/when (partial mocking) segítségével. Ez a spy-objektum kiválasztott metódusaihoz a mock-hoz hasonló viselkedést biztosít.
Kötelező. A Swift-ben nincs dinamikus proxyzás, mint Java/Kotlin esetében. A spy létrehozásához egy protokollra van szükség, amelyet mind a termelési osztály, mind a spy osztály implementál. A Swift-protocol-based spy egy manuális implementáció, amely átvesz egy valós objektumot, delegálja neki a hívásokat és rögzíti a metaadatokat. Alternatíva: a Cuckoo könyvtár, amely SourceKit segítségével generál spy osztályokat.
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is