Spy (Spion) — ein Testobjekt, das eine reale Instanz umhüllt und Informationen über jeden Aufruf aufzeichnet: welche Methoden aufgerufen wurden, mit welchen Argumenten und wie oft. Im Gegensatz zu einem Mock verwendet ein Spy die reale Implementierung des umhüllten Objekts — Aufrufe durchlaufen den tatsächlichen Code, und der Spy zeichnet nur die Fakten auf. Nach der Testausführung überprüft der Entwickler die Aufzeichnungen des Spions: „Wurde die Methode sendAnalytics dreimal aufgerufen?“. Weitere Informationen finden Sie im Android-Testleitfaden.
Wichtige Erkenntnisse
Spy — ein Wrapper um ein reales Objekt, der alle Methodenaufrufe abfängt und aufzeichnet. Die tatsächliche Logik des Objekts wird ausgeführt: wenn die Methode Daten speichert, einen Wert berechnet oder eine Anfrage stellt — alles geschieht wie gewohnt. Zusätzlich zeichnet der Spy Metadaten auf: Methodenname, Argumente, Anzahl der Aufrufe, Ausführungszeit. Der Begriff ist Teil der Klassifikation von Meszaros (2007) und wird im Artikel „Mocks Aren’t Stubs“ von Martin Fowler detailliert beschrieben.
Hauptunterschied zu einem Mock — ein Mock ersetzt das Objekt vollständig durch einen Test-Stub; alle Methoden tun standardmäßig nichts. Ein Spy umhüllt ein vorhandenes Objekt: alle Methoden funktionieren standardmäßig wie gewohnt, werden aber auch aufgezeichnet. Dieser Unterschied ist grundlegend: ein Mock isoliert den getesteten Code von der Realität, während ein Spy die Realität bewahrt und ermöglicht, sie zu beobachten. Die Wahl zwischen ihnen hängt davon ab, was getestet wird.
Ein Spy ist die richtige Wahl — wenn der getestete Code den Zustand eines realen Objekts ändert und der Test sowohl das Ergebnis (Zustand) überprüfen als auch sicherstellen muss, dass die Aufrufe in der richtigen Reihenfolge erfolgten. Ein Mock ist nicht geeignet, da er die tatsächliche Implementierung nicht ausführt. Ein Stub ist nicht geeignet, da er keine Aufrufe aufzeichnet. Ein Spy ist der einzige Test-Double, der gleichzeitig die reale Logik bewahrt und Informationen über Aufrufe liefert.
Mock — vollständige Isolation. Wenn der Test nicht von der Implementierung des realen Objekts abhängen soll (z. B. Datenbank oder Netzwerk-Client), verwenden Sie einen Mock. Ein Mock stellt sicher, dass kein Aufruf die tatsächliche Komponente erreicht. Dies ist sicher und vorhersagbar. Der Nachteil: ein Mock führt keine reale Logik aus. Wenn der getestete Code also von einem Rückgabewert abhängt, muss dieser explizit über when/stub konfiguriert werden.
Spy — reale Logik + Beobachtung. Wenn der getestete Code mit einem Objekt interagiert, dessen Logik für den Test wichtig ist, und nicht nur Daten — verwenden Sie einen Spy. Zum Beispiel ein AnalyticsTracker, der Ereignisse sammelt und periodisch sendet. Der Test überprüft, dass Ereignisse zum Puffer hinzugefügt wurden und der Puffer nach dem Senden geleert wurde. Ein Mock kann dies nicht überprüfen, da er die tatsächliche Logik des Trackers nicht ausführt.
| Szenario | Spy | Mock |
|---|---|---|
| Reale Logik benötigt | Ja | Nein (Stub) |
| Aufrufverifikation | Ja (Anzahl, Argumente) | Ja (Anzahl, Argumente) |
| Teilweises Stubbing | Ja (einige Methoden Spy, andere Stub) | Nein (alle Methoden Stubs) |
| Risiko von Nebenwirkungen | Hoch (echter Code) | Keines |
| Geschwindigkeit | Niedriger (reale Logik) | Höher (Stubs) |
| Lesbarkeit | Niedriger (schwerer zu verstehen, was real ist) | Höher (alles ist explizit) |
Spy für alles — die Verwendung eines Spys anstelle eines Mocks für alle Tests ist ein Fehler. Ein Spy führt echten Code aus, der Nebenwirkungen haben kann: Schreiben in eine Datei, HTTP-Senden, Ändern des globalen Zustands. Wenn das getestete Modul eine Methode eines Spy-Objekts aufruft, die eine HTTP-Anfrage stellt, wird der Test zu einem Integrationstest, nicht zu einem Unit-Test. Regel: wenn ein Spy ein Objekt mit I/O-Operationen umhüllt — ist es kein Unit-Test mehr. Verwenden Sie Mocks zur I/O-Isolation und Spions nur für In-Memory-Objekte ohne externe Effekte.
Mockito.spy() — die klassische Methode, um einen Spion in Java/Kotlin-Projekten zu erstellen. spy() nimmt ein reales Objekt entgegen und gibt einen Wrapper zurück. Alle Aufrufe werden standardmäßig an das reale Objekt delegiert und die Ergebnisse aufgezeichnet. Nach dem Test können Sie mit verify() die Anzahl der Aufrufe und Argumente überprüfen. Für Methoden, die Testdaten zurückgeben sollen, wird doReturn/when verwendet — dies wird als „Partial Mocking“ bezeichnet.
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() — eine Alternative für Kotlin-Projekte mit besserer Unterstützung für Coroutinen und versiegelte Klassen. MockK.spyk() erstellt einen Spion, analog zu Mockito.spy(). Es unterstützt coVerify für Suspend-Funktionen und every für teilweises Stubbing. Im Gegensatz zu Mockito unterstützt MockK keine Spions für finale Klassen (alle Klassen in Kotlin sind standardmäßig final) — Sie müssen die Klasse öffnen (open) oder ein Interface verwenden.
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)
}
}
Teilweises Stubbing — eine leistungsstarke, aber gefährliche Technik. Sie können einen Spy eines Objekts erstellen und nur einige Methoden überschreiben (stubben), während die anderen real bleiben. Beispiel: ein Spy-Repository, bei dem getUser() Testdaten zurückgibt, während saveUser() tatsächlich in einer In-Memory-Liste speichert. Dies ermöglicht die Kombination der Vorteile von Stubs (kontrollierte Daten) und Spions (reale Logik). Der Nachteil: die Lesbarkeit des Tests leidet — es ist nicht offensichtlich, welche Methoden real und welche Stubs sind.
OCMock für Objective-C — eine Bibliothek, die die Erstellung von Spy-Objekten über niceMock unterstützt. OCMock fängt Methodenaufrufe mit der Objective-C-Laufzeit ab und zeichnet sie auf. Nach dem Test wird verify aufgerufen. OCMock unterstützt Spions für jedes Objekt (alle Methoden in Objective-C sind dynamisch), was einen Vorteil gegenüber Swift bietet, wo Spions nur über Protokolle möglich sind.
// Erstellen eines Spions für ein reales Objekt
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];
// Ausführen des Tests
[spy trackEvent:@"login"];
// Verifikation
[[spy verify] trackEvent:@"login"];
XCTAssertEqual([realTracker eventCount], 1);
Swift-Protokoll-basierter Spy — Swift hat keine Laufzeitreflexion wie Objective-C, daher werden Spions manuell erstellt. Eine Teststruktur implementiert ein Protokoll und ruft intern das reale Objekt auf, während sie gleichzeitig die Aufrufe aufzeichnet. Dies erfordert mehr Code, ist aber vollständig kontrollierbar und typsicher. Manuelle Spions benötigen keine externen Bibliotheken und verwenden keine Laufzeit — alles wird zur Compile-Zeit überprüft.
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)
}
}
Wann OCMock vs manueller Spy — für Objective-C-Code verwenden Sie OCMock (weniger Boilerplate). Für Swift sind manuelle Spions über Protokolle zu bevorzugen. Ein manueller Spy gibt volle Kontrolle über die Aufrufaufzeichnung, benötigt keine Reflexion und funktioniert mit Werttypen (Structs). Der einzige Nachteil: Sie müssen den Spy-Klassen-Code beim Hinzufügen neuer Methoden mit dem Protokoll synchron halten.
Analyse-Überprüfung — der häufigste Anwendungsfall für Spions. Im Produktionscode sind Analyseaufrufe über die gesamte Anwendung verteilt: Login, Logout, Kauf, Fehler. Der Test erstellt einen Spy-Wrapper für AnalyticsTracker, führt ein Szenario aus (Login, Produkt ansehen, in den Warenkorb legen, kaufen) und überprüft, ob alle erforderlichen Ereignisse in der richtigen Reihenfolge gesendet wurden. Ein Mock ist nicht geeignet, da AnalyticsTracker Pufferungs- und Sendelogik enthält.
Timer und Planer — das Testen von Code, der Handler (Android) oder Timer (iOS) verwendet, ist aufgrund der Echtzeit schwierig. Ein Spy-Wrapper für Scheduler zeichnet auf, welche Aufgaben mit welcher Verzögerung geplant wurden. Der Test erstellt einen Spy des realen Handlers, führt eine Aktion aus und überprüft, ob Handler.postDelayed(runnable, delay) mit der richtigen Verzögerung aufgerufen wurde. Die eigentliche Aufgabe wird nicht ausgeführt — der Spy fängt den Aufruf ab und zeichnet ihn auf.
Protokollierung und Debug-Informationen — in der Produktion können Protokolle deaktiviert oder in eine Datei geschrieben werden. Ein Spy-Wrapper für Logger zeichnet alle Nachrichten in einer In-Memory-Liste auf, die der Test nach der Ausführung überprüft. Dies ermöglicht die Überprüfung, ob bei einem Fehler die korrekte Nachricht geschrieben wurde, ohne die Konsole zu überladen. Manuelle Spions für Logger sind besonders auf iOS nützlich, wo OSLog keine Test-API hat.
Überprüfung der Aufrufreihenfolge — einige Szenarien erfordern eine strikte Reihenfolge der Operationen: Verbindung öffnen, Daten senden, Verbindung schließen. Mockito ermöglicht die Überprüfung der Reihenfolge über InOrder.verify(). Ein Spy macht dasselbe, bewahrt aber die tatsächliche Ausführung. Wenn sowohl die Reihenfolge als auch das Ergebnis jedes Schritts (die Verbindung wurde tatsächlich geöffnet) wichtig sind — verwenden Sie einen Spy, keinen Mock.
Häufig gestellte Fragen
Spy umhüllt ein reales Objekt und führt seine Logik aus, zusätzlich zeichnet es Aufrufe auf. Mock ersetzt das Objekt vollständig durch einen Stub — keine reale Logik wird ausgeführt. Ein Spy bewahrt das Verhalten, ein Mock nicht. Wählen Sie einen Spy, wenn die tatsächliche Arbeit des Objekts wichtig ist; wählen Sie einen Mock, wenn Sie den Test von einer externen Abhängigkeit isolieren müssen.
Wenn ein Spy-Wrapper zu echten I/O-Operationen führt. Wenn ein Spy ein Objekt umhüllt, das in eine Datei schreibt, HTTP sendet oder von der Festplatte liest — ist der Test kein Unit-Test mehr. Zweiter Fall: der Test überprüft nur den Rückgabewert, ohne sich für Aufrufe zu interessieren — hier reicht ein Stub, und ein Spy ist überflüssig. Dritter: der Code verlässt sich auf den internen Zustand des Spys — das ist ein fragiler Test.
Ja, über spyk() — das Äquivalent zu Mockito.spy(). MockK.spyk() erstellt einen Spion um ein reales Objekt, unterstützt every für teilweises Stubbing und coVerify/coroutinesVerify für Suspend-Funktionen. Einschränkung: funktioniert nicht mit finalen Klassen (benötigt open oder Interface). Für Java-Klassen unterstützt MockK ebenfalls spyk(), benötigt aber die Annotation @MockKJvmInline.
Technisch gesehen nein. Mock ist ein Stub, der keine reale Implementierung enthält. Ein Spy umhüllt per Definition ein reales Objekt. In Mockito können Sie einen Mock nicht in einen Spy verwandeln. Aber Sie können das Gegenteil tun: einen Spy erstellen und einige Methoden über doReturn/when (Partial Mocking) überschreiben. Dies ergibt ein Mock-ähnliches Verhalten für ausgewählte Methoden des Spy-Objekts.
Ja, das ist erforderlich. In Swift gibt es keine dynamische Proxy-Funktion wie in Java/Kotlin. Zum Erstellen eines Spions wird ein Protokoll benötigt, das sowohl die Produktionsklasse als auch die Spy-Klasse implementieren. Ein Swift-Protokoll-basierter Spy ist eine manuelle Implementierung, die ein reales Objekt annimmt, Aufrufe an es delegiert und Metadaten aufzeichnet. Alternative: die Bibliothek Cuckoo, die Spy-Klassen über SourceKit generiert.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch