Spy (spion) — ett testobjekt som omsluter en verklig instans och registrerar information om varje anrop: vilka metoder som anropades, med vilka argument, hur många gånger. Till skillnad från mock använder spy den verkliga implementationen av det omslutna objektet — anrop går genom riktig kod och spy registrerar bara fakta. Efter testets utförande kontrollerar utvecklaren spionens anteckningar: „Anropades metoden sendAnalytics tre gånger?”. Mer — i Androids testguide.
Huvudpunkter
Spy — är ett hölje runt ett verkligt objekt som fångar upp alla metodanrop och registrerar dem. Objektets verkliga logik utförs: om metoden sparar data, beräknar ett värde eller gör en begäran — allt sker som vanligt. Dessutom registrerar spy metadata: metodens namn, argument, antal anrop, exekveringstid. Termen ingår i Meszaros (2007) klassificering och beskrivs detaljerat i Martin Fowler artikel „Mocks Aren't Stubs”.
Viktigaste skillnaden från Mock — mock ersätter helt objektet med en test-stub, alla metoder gör som standard ingenting. Spy omsluter ett befintligt objekt: alla metoder fungerar som standard som vanligt, men registreras samtidigt. Denna skillnad är grundläggande: mock isolerar den testade koden från verkligheten, spy bevarar verkligheten och gör det möjligt att observera den. Valet mellan dem beror på vad som testas.
Spy — rätt val — om den testade koden modifierar tillståndet hos ett verkligt objekt och testet både måste kontrollera resultatet (tillståndet) och försäkra sig om att anropen gjordes i rätt ordning. Mock är inte lämplig eftersom den inte utför den verkliga implementationen. Stub är inte lämplig eftersom den inte registrerar anrop. Spy är den enda testdubbel som samtidigt bevarar den verkliga logiken och ger information om anrop.
Mock — fullständig isolering. Om testet inte bör vara beroende av implementationen av ett verkligt objekt (till exempel databas eller nätverksklient), använd mock. Mock garanterar att inget anrop når den verkliga komponenten. Detta är säkert och förutsägbart. Nackdel: mock utför inte verklig logik, så om den testade koden förlitar sig på returvärdet — måste det konfigureras explicit via when/stub.
Spy — verklig logik + observation. Om den testade koden interagerar med ett objekt vars logik är viktig för testet, inte bara data — använd spy. Till exempel AnalyticsTracker som samlar in händelser och skickar dem periodiskt. Testet kontrollerar att händelser har lagts till i bufferten och att bufferten efter sändning har tömts. Mock kan inte kontrollera detta eftersom den inte utför trackerens verkliga logik.
| Scenario | Spy | Mock |
|---|---|---|
| Verklig logik behövs | Ja | Nej (stub) |
| Verifiering av anrop | Ja (antal, argument) | Ja (antal, argument) |
| Delvis stubbning | Ja (vissa metoder — spy, andra — stub) | Nej (alla metoder — stub) |
| Risk för bieffekter | Hög (verklig kod) | Noll |
| Hastighet | Lägre (verklig logik) | Högre (stubbar) |
| Läsbarhet | Lägre (svårare att förstå vad som är verkligt) | Högre (allt explicit) |
Spy för allt — att använda spy istället för mock i alla test är ett misstag. Spy exekverar verklig kod som kan ha bieffekter: skriva till fil, skicka HTTP, ändra globalt tillstånd. Om den testade modulen anropar en metod på ett spy-objekt som gör en HTTP-förfrågan, blir testet ett integrationstest, inte ett enhetstest. Regel: om spy omsluter ett objekt med I/O-operationer — är detta inte längre ett enhetstest. Använd mock för I/O-isolering, spy — endast för in-memory-objekt utan externa effekter.
Mockito.spy() — det klassiska sättet att skapa en spion i Java/Kotlin-projekt. spy() tar emot ett verkligt objekt och returnerar ett hölje. Alla anrop delegeras som standard till det verkliga objektet och resultaten registreras. Efter testets utförande kan man via verify() kontrollera antalet anrop och argument. För metoder som ska returnera testdata används doReturn/when — detta kallas „delvis stubbning” (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 för Kotlin-projekt med bättre stöd för korutiner och sealed-klasser. MockK.spyk() skapar en spion, analogt med Mockito.spy(). Stöder coVerify för suspend-funktioner och every för delvis stubbning. Till skillnad från Mockito stöder MockK inte spy för final-klasser (alla klasser i Kotlin är som standard final) — klassen måste öppnas (open) eller ett gränssnitt måste användas.
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)
}
}
Delvis stubbning — en kraftfull men farlig teknik. Du kan skapa en spy av ett objekt och skriva över (stubba) endast vissa metoder, medan de andra lämnas verkliga. Exempel: ett spy-repository vars getUser() returnerar testdata och saveUser() faktiskt sparar i en in-memory-lista. Detta gör det möjligt att kombinera fördelarna med stubbar (kontrollerad data) och spy (verklig logik). Nackdel: komplexiteten i att läsa testet — det är inte uppenbart vilka metoder som är verkliga och vilka som är stubbar.
OCMock för Objective-C — bibliotek som stöder skapande av spy-objekt via niceMock. OCMock fångar upp metodanrop med hjälp av Objective-C-runtime och registrerar dem. Efter testets utförande anropas verify. OCMock stöder spy för vilket objekt som helst (i Objective-C är alla metoder dynamiska), vilket ger en fördel jämfört med Swift, där spy endast är möjligt via protokoll.
// Skapa spy för det verkliga objektet
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];
// Utför testet
[spy trackEvent:@"login"];
// Verifiering
[[spy verify] trackEvent:@"login"];
XCTAssertEqual([realTracker eventCount], 1);
Swift protocol-based spy — i Swift finns ingen Objective-C-runtime-reflektion, så spy skapas manuellt. Teststrukturen implementerar protokollet och anropar internt det verkliga objektet, samtidigt som den registrerar anrop. Detta är mer kod, men fullt kontrollerbart och typsäkert. Manuella spioner kräver inga externa bibliotek och använder inte runtime — allt kontrolleras i kompileringsfasen.
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)
}
}
När du använder OCMock vs manuell spy — för Objective-C-kod, använd OCMock (mindre boilerplate). För Swift — föredras manuella spioner via protokoll. Manuell spy ger full kontroll över registreringen av anrop, kräver ingen reflektion och fungerar med värdetyper (struct). Den enda nackdelen: spy-klassens kod måste hållas synkroniserad med protokollet när nya metoder läggs till.
Kontroll av analys — det vanligaste scenariot för användning av spy. I produktionskod är analysanrop spridda över hela applikationen: login, logout, purchase, error. Testet skapar ett spy-hölje för AnalyticsTracker, utför scenariot (inloggning, produktvy, lägg i kundvagn, köp) och kontrollerar att alla nödvändiga händelser skickades i rätt ordning. Mock är inte lämplig eftersom AnalyticsTracker innehåller buffrings- och sändningslogik.
Timer och schemaläggare — testning av kod som använder Handler (Android) eller Timer (iOS) är svår på grund av realtid. Spy-hölje för Scheduler registrerar vilka uppgifter som har schemalagts och med vilken fördröjning. Testet skapar en spy av den verkliga Handlern, utför åtgärden och kontrollerar att Handler.postDelayed(runnable, delay) anropades med rätt fördröjning. Den verkliga uppgiften utförs inte under tiden — spy fångar upp och registrerar anropet.
Loggning och felsökningsinformation — i produktion kan loggar vara avstängda eller skrivna till fil. Spy-hölje för Logger registrerar alla meddelanden i en in-memory-lista som testet kontrollerar efter körning. Detta gör det möjligt att kontrollera att rätt meddelande skrivs vid fel, utan att skräpa ner konsolen. Manuella spioner för Logger är särskilt användbara på iOS, där OSLog inte har ett test-API.
Kontroll av anropsordning — vissa scenarier kräver en strikt ordning av operationer: öppna anslutning, skicka data, stäng anslutning. Mockito gör det möjligt att kontrollera ordningen via InOrder.verify(). Spy gör samma sak men bevarar den verkliga exekveringen. Om inte bara ordningen utan även resultatet av varje steg är viktigt (anslutningen öppnades verkligen) — använd spy, inte mock.
Vanliga frågor
Spy omsluter ett verkligt objekt och utför dess logik, samt registrerar anrop. Mock ersätter helt objektet med en stub — ingen verklig logik utförs. Spy bevarar beteendet, mock inte. Välj spy när objektets verkliga arbete är viktigt; välj mock när testet måste isoleras från ett externt beroende.
När spy-hölje leder till verkliga I/O-operationer. Om spy omsluter ett objekt som skriver till fil, skickar HTTP eller läser från disk — upphör testet att vara ett enhetstest. Andra fallet: testet kontrollerar bara returvärdet utan intresse för anrop — här räcker stub och spy är överflödig. Tredje: koden förlitar sig på spions interna tillstånd — detta är ett bräckligt test.
Ja, via spyk() — analogt med Mockito.spy(). MockK.spyk() skapar en spion runt ett verkligt objekt, stöder every för delvis stubbning och coVerify/coroutinesVerify för suspend-funktioner. Begränsning: fungerar inte med final-klasser (open eller gränssnitt krävs). För Java-klasser stöder MockK också spyk(), men kräver annoteringen @MockKJvmInline.
Tekniskt — nej. Mock är en stub som inte innehåller någon verklig implementation. Spy omsluter per definition ett verkligt objekt. I Mockito kan man inte omvandla en mock till en spy. Men det omvända är möjligt: skapa en spy och skriv över en del av metoderna via doReturn/when (partial mocking). Detta ger ett beteende som liknar mock för utvalda metoder på spy-objektet.
Obligatoriskt. I Swift finns ingen dynamisk proxy som i Java/Kotlin. För att skapa en spy krävs ett protokoll som både produktionsklassen och spy-klassen implementerar. Swift-protocol-based spy är en manuell implementation som tar emot ett verkligt objekt, delegerar anrop till det och registrerar metadata. Alternativ: biblioteket Cuckoo, som genererar spy-klasser via SourceKit.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också