Test Doubles sind Ersatzobjekte, die in Modultests anstelle von echten Abhängigkeiten verwendet werden. Der Begriff wurde von Gerard Meszaros im Buch „xUnit Test Patterns“ (2007) als übergeordnetes Konzept für Mock, Stub, Fake, Spy und Dummy eingeführt. Laut Martin Fowler (2024) ermöglichen Test Doubles die Isolierung der zu testenden Komponente von ihrer Umgebung, wodurch Tests deterministisch, schnell und unabhängig von externen Diensten werden.
Wichtige Punkte
Test Doubles ist ein Begriff aus der Automobilindustrie (Stuntdouble), der in die Softwareentwicklung übertragen wurde. Wie ein Stuntdouble einen Schauspieler in einer gefährlichen Szene ersetzt, ersetzt ein Test Double eine echte Komponente in einem Testszenario. Dies ist notwendig, wenn die echte Abhängigkeit nicht verfügbar, langsam, nicht deterministisch ist oder Nebenwirkungen hat.
Das Konzept der Test Doubles umfasst fünf spezifische Typen, die jeweils ihre eigene Aufgabe lösen. Die Typologie von Meszaros ist kanonisch und wird in allen modernen Testleitfäden verwendet. Der Unterschied zwischen den Typen liegt im Grad der Kontrolle und Überprüfung: vom einfachen Füllen von Parametern (Dummy) bis zur vollständigen Überprüfung der Aufrufsequenz (Mock).
Der Hauptzweck von Test Doubles ist die Isolierung des zu testenden Moduls. In der mobilen Entwicklung umfassen echte Abhängigkeiten API-Server, Datenbanken, Dateisysteme, Gerätesensoren und Systemdienste (LocationManager, Camera, Bluetooth). Die direkte Verwendung dieser Komponenten macht Tests langsam, zerbrechlich und umgebungsabhängig. Laut Google Testing Blog (2023) werden gut isolierte Modultests in Millisekunden ausgeführt, während Integrationstests in Sekunden und Minuten ausgeführt werden.
Die Klassifikation von Gerard Meszaros umfasst fünf Arten von Test Doubles, die sich in Verhalten und Zweck unterscheiden. Den Unterschied zwischen ihnen zu verstehen, ist die Grundlage für kompetente Modultests.
Dummy ist ein Objekt, das an die zu testende Methode übergeben wird, aber nie verwendet wird. Dummy wird nur benötigt, um die Methodensignatur zu erfüllen. In Kotlin ist dies oft null, emptyList() oder ein Objekt mit Stubs. Dummy sollte keine Logik enthalten — wenn es aufgerufen wird, sollte der Test fehlschlagen.
Fake ist eine vereinfachte, aber funktionsfähige Implementierung einer Schnittstelle. Im Gegensatz zu Mock und Stub enthält Fake echte Geschäftslogik, jedoch in vereinfachter Form. Ein klassisches Beispiel ist InMemoryUserRepository, das Daten in einer HashMap statt in einer Datenbank speichert. Fake wird verwendet, wenn Logik getestet werden muss, die vom Zustand abhängt, aber ohne den Overhead der echten Infrastruktur.
| Typ | Zweck | Beispiel |
|---|---|---|
| Dummy | Parameter füllen | null, leeres Objekt |
| Fake | Funktionsfähige vereinfachte Implementierung | InMemoryRepository |
| Stub | Festgelegten Wert zurückgeben | when(api.getUser()).thenReturn(user) |
| Spy | Aufrufe zur Überprüfung aufzeichnen | verify(spy).save(user) |
| Mock | Interaktion überprüfen | verify(mock).sendEmail(email) |
Stub gibt vordefinierte Werte für bestimmte Aufrufe zurück. Stub prüft nicht, ob er aufgerufen wurde — er liefert einfach Daten. In Mockito wird Stub über when(method).thenReturn(value) erstellt. Stub ist ideal für Tests, wenn eine Abhängigkeit einen bestimmten Wert zurückgeben soll, der Aufruf selbst jedoch nicht wichtig ist.
Spy ist ein Wrapper um ein echtes Objekt, der alle Aufrufe zur späteren Überprüfung aufzeichnet. Im Gegensatz zu Mock delegiert Spy Aufrufe an das echte Objekt, erlaubt aber die Überprüfung, dass sie stattgefunden haben. In Mockito wird Spy über spy(realObject) erstellt. Spy ist nützlich für partielles Mocking, wenn man ein echtes Objekt verwenden, aber einige Aufrufe überprüfen möchte.
Mock ist ein Objekt mit vordefinierten Aufruferwartungen. Mock überprüft, dass bestimmte Methoden mit bestimmten Argumenten und in einer bestimmten Reihenfolge aufgerufen wurden. Im Gegensatz zu Stub konzentriert sich Mock auf die Verhaltensüberprüfung und nicht auf die Datenrückgabe. Mock ist der leistungsfähigste und am häufigsten verwendete Test-Double-Typ in der mobilen Entwicklung.
Der Unterschied zwischen Mock und Stub verursacht selbst bei erfahrenen Entwicklern häufig Verwirrung. Der Hauptunterschied liegt im Zweck: Stub prüft den Zustand (state verification), Mock prüft das Verhalten (behavior verification).
Stub beantwortet die Frage: „Hat der Code das richtige Ergebnis zurückgegeben?“. Mock beantwortet die Frage: „Hat der Code die richtigen Methoden mit den richtigen Argumenten aufgerufen?“. In der mobilen Entwicklung wird Stub verwendet, wenn das Ergebnis wichtig ist (z. B. Daten aus einem Repository), während Mock verwendet wird, wenn Nebenwirkungen wichtig sind (z. B. E-Mail senden, in eine Datenbank schreiben).
// Stub: Zustandsprüfung
every { repository.getUsers() } returns listOf(user)
val result = useCase.getUsers()
assertEquals(1, result.size)
// Mock: Verhaltensprüfung
every { analytics.logEvent("purchase") } returns Unit
useCase.purchase(item)
verify { analytics.logEvent("purchase") }
Praktische Beispiele aller fünf Typen von Test Doubles in Kotlin mit MockK — der beliebtesten Mocking-Bibliothek für Android-Projekte.
class InMemoryUserRepository : UserRepository {
private val store = mutableMapOf<String, User>()
override fun save(user: User) {
store[user.email] = user
}
override fun findByEmail(email: String): User? {
return store[email]
}
}
class RegisterUseCaseTest {
private val api = mockk<AuthApi>()
private val repo = spyk(InMemoryUserRepository())
private val useCase = RegisterUseCase(api, repo)
fun `register user successfully`() = runTest {
// Stub: feste API-Antwort zurückgeben
coEvery { api.register("test@test.com") } returns AuthResult.Success("token123")
val result = useCase.execute("test@test.com")
// Verify: prüfen, dass der Benutzer gespeichert wurde
verify { repo.save(any()) }
assertTrue(result.isSuccess())
}
}
data class Logger(val appContext: Context, val format: FormatType)
fun `test logger with dummy context`() {
// Dummy: Context wird innerhalb von Logger nicht verwendet
val dummyContext = mockk<Context>()
val logger = Logger(dummyContext, FormatType.JSON)
assertEquals(FormatType.JSON, logger.format)
}
Die Wahl des Test Double-Typs hängt davon ab, was genau getestet wird: Zustand, Verhalten oder Integration. In der mobilen Entwicklung für Android und iOS haben sich die folgenden Empfehlungen etabliert.
Beim Testen von ViewModel verwenden Sie Mock für Abhängigkeiten, die Nebenwirkungen erzeugen (Repositories, Analytics, Navigation), und Stub für Abhängigkeiten, die Daten zurückgeben (API-Clients, ContentProvider). Dies ermöglicht die Überprüfung, dass das ViewModel sowohl Erfolgs- als auch Fehlerszenarien korrekt behandelt.
Auf Repository-Ebene bevorzugen Sie Fake (In-Memory-Datenbankimplementierungen) und Stub (feste API-Antworten). Fake ermöglicht das Testen von Caching-Logik und Offline-Modus ohne Einrichtung von SQLite. Stub simuliert verschiedene HTTP-Status: 200, 404, 500, Timeout.
Die falsche Verwendung von Test Doubles ist eine der häufigsten Ursachen für zerbrechliche Tests, die bei jeder Refaktorisierung brechen.
Der häufigste Fehler ist, alles zu mocken. Wenn jede Abhängigkeit in einem Test durch einen Mock ersetzt wird, hört der Test auf, das tatsächliche Verhalten zu überprüfen. Mock sollte nur für externe Abhängigkeiten verwendet werden (Netzwerk, Datenbank, Dateisystem, Systemdienste). Interne Anwendungskomponenten (Value Object, Data Class, einfache Hilfsprogramme) sollten nicht ersetzt werden.
Der zweite Fehler ist das Erstellen eines Mocks ohne Definition von Erwartungen. Wenn eine Methode ohne every / when aufgerufen wird, gibt Mock einen Standardwert zurück (null, 0, false). Dies kann zu falsch-positiven Tests führen, bei denen Mock stillschweigend null zurückgibt und der Test dies als korrektes Verhalten interpretiert.
Der dritte Fehler ist die Überprüfung jedes Aufrufs jedes Mocks. Verify sollte nur für Aufrufe verwendet werden, die aus Sicht der Geschäftslogik kritisch wichtig sind. Übermäßige Überprüfung macht Tests zerbrechlich: Eine Änderung der Aufrufreihenfolge im Produktionscode zerbricht Tests, ohne das Verhalten zu ändern.
Häufig gestellte Fragen
Stub gibt Daten zurück und prüft den Zustand (was zurückgegeben wurde), während Mock das Verhalten prüft (welche Methoden aufgerufen wurden). Stub = „gib X zurück“, Mock = „prüfe, dass Y mit Argument Z aufgerufen wurde“. In echten Tests fungiert ein Objekt oft gleichzeitig als Stub und Mock.
Fake ist Mock vorzuziehen, wenn Logik getestet wird, die vom Zustand abhängt: Caching, Offline-Modus, Transaktionen. Fake (In-Memory-Implementierung) ermöglicht das Testen dieser Szenarien ohne zerbrechliche Verify-Aufrufe. Mock eignet sich besser zur Überprüfung des Datenversands: Analytics, Push, E-Mail.
Für Android-Projekte in Kotlin wird MockK empfohlen. Es unterstützt Coroutinen, Suspend-Funktionen, versiegelte Klassen und Erweiterungsfunktionen ohne zusätzliche Konfiguration. Für Java-Projekte bleibt Mockito der Standard — die beliebteste Bibliothek mit umfangreicher Dokumentation.
Zum Testen von Kotlin Flow verwenden Sie die Bibliothek Turbine zusammen mit MockK. Turbine vereinfacht die Überprüfung von Flow-Emissionen: Sie können die Reihenfolge der Werte, den Abschluss des Streams und Ausnahmen überprüfen. Stub für Flow gibt flowOf(value) zurück, Mock prüft, ob der Flow gesammelt wurde.
Ja, aber auf der Ebene der API-Antworten, nicht der UI-Komponenten. Die Bibliotheken MockWebServer (OkHttp) und WireMock ermöglichen das Mocken von HTTP-Antworten in UI-Tests. Die UI-Komponenten selbst (Compose, SwiftUI Views) sollten nicht ersetzt werden — ihr Verhalten wird durch Screenshot-Tests und Espresso getestet.
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