Test-Abhängigkeiten ersetzen
Wie du Produktionsabhängigkeiten in Tests durch Fakes ersetzt, ohne Feature-Code anzufassen. Sauberere Isolation, schnellere CI-Pipelines.
Wer Android-Code testet, stößt früh auf ein praktisches Problem: Die Klasse, die du prüfen willst, hängt von einem Netzwerk-Repository, einer Datenbank oder einem System-Sensor ab – Dinge, die im Test unkontrollierbar, langsam oder schlicht nicht verfügbar sind. Test Dependency Replacement löst genau dieses Problem, ohne dass du dafür eine einzige Zeile Feature-Code anfassen musst.
Was ist das?
Test Dependency Replacement bezeichnet die Praxis, eine Produktionsimplementierung in Tests durch eine alternative, kontrollierte Implementierung zu ersetzen – typischerweise durch ein Fake. Der entscheidende Punkt: Die Ersetzung passiert ausschließlich in der Testkonfiguration. Die eigentliche Geschäftslogik – dein ViewModel, dein UseCase oder dein Repository – kennt nur das Interface oder die abstrakte Klasse, nicht die konkrete Implementierung dahinter.
In der Android-Architektur ist dieser Ansatz fest verankert. Das offizielle Architecture-Leitbild empfiehlt, jede Schicht gegen Abstraktionen zu binden, nicht gegen konkrete Implementierungen. Genau das macht die spätere Ersetzung im Test möglich: Sobald ein UserRepository ein Interface ist, kannst du im Test eine FakeUserRepository-Instanz einsetzen, die Daten deterministisch zurückgibt, anstatt eine echte Netzwerkanfrage auszulösen.
Fakes unterscheiden sich von Mocks durch ihren Charakter. Ein Mock ist ein generiertes Objekt, das du nach dem Erzeugen per when(...).thenReturn(...) instruierst – oft spröde, weil die Verhaltenserwartungen direkt im Test hart kodiert sind. Ein Fake ist eine schlanke, manuell geschriebene Klasse, die das Interface vollständig implementiert und sich wie eine echte Implementierung verhält, nur mit vorher festgelegten Werten. Fakes sind einfacher zu lesen, einfacher zu warten und produzieren aussagekräftigere Fehlermeldungen.
Wie funktioniert es?
Der Mechanismus besteht aus drei Elementen: einem Interface, einer Fake-Klasse und einer Abhängigkeitsinjektion.
Interface definieren
Zuerst extrahierst du ein Interface aus deinem Repository oder Service:
interface UserRepository {
suspend fun getUser(id: String): User
suspend fun saveUser(user: User)
}
Die Produktionsimplementierung (RealUserRepository) und die Test-Implementierung (FakeUserRepository) implementieren dasselbe Interface. Der Feature-Code – zum Beispiel ein ViewModel – kennt nur UserRepository, nicht die konkreten Klassen dahinter.
Fake-Implementierung schreiben
class FakeUserRepository : UserRepository {
private val users = mutableMapOf<String, User>()
var shouldThrowError = false
override suspend fun getUser(id: String): User {
if (shouldThrowError) throw IOException("Netzwerkfehler simuliert")
return users[id] ?: error("User $id nicht gefunden")
}
override suspend fun saveUser(user: User) {
users[user.id] = user
}
}
Das Flag shouldThrowError ist ein wichtiges Detail: Mit einem einzigen Attribut kannst du Fehlerpfade testen, ohne echte Netzwerkprobleme zu provozieren. Das hält den Test deterministisch und reproduzierbar.
Abhängigkeit per DI austauschen
Mit Hilt tauschst du im Test das Produktionsmodul durch ein Testmodul aus:
@HiltAndroidTest
class UserViewModelTest {
@BindValue
val userRepository: UserRepository = FakeUserRepository()
@get:Rule
val hiltRule = HiltAndroidRule(this)
// Tests ...
}
Keine Änderung am ViewModel, keine Änderung am Feature-Code. Nur die Testinfrastruktur weiß, dass hier ein Fake im Einsatz ist.
In der Praxis
Fakes in der Modulstruktur
In einem multi-modularen Projekt lohnt es sich, Fakes in einem dedizierten Quellsatz unterzubringen – entweder als androidTest-Quellsatz im jeweiligen Datenmodul oder in einem eigenen :testing-Hilfemodul. Dadurch können mehrere Feature-Module dieselben Fakes wiederverwenden, ohne dass interne Implementierungsdetails nach außen dringen. Das Architecture-Recommendations-Dokument von Google bestätigt genau diesen Schnitt: Teste-Hilfsklassen gehören zum Datenmodul, nicht zum Feature-Modul.
Typische Stolperfalle: Fake nicht synchron halten
Der häufigste Fehler ist ein Fake, der hinter der echten Interface-Implementierung zurückbleibt. Wenn du in UserRepository eine neue Methode deleteUser() hinzufügst, aber FakeUserRepository nicht aktualisierst, kompilieren die Tests nicht mehr – das ist gut, der Compiler hilft dir. Gefährlicher ist die umgekehrte Situation: Du änderst das Verhalten einer bestehenden Methode subtil (zum Beispiel eine neue Exception-Klasse für einen bestimmten Fehlerfall), vergisst aber, den Fake anzupassen. Die Unit-Tests bleiben grün, weil der Fake das alte Verhalten simuliert, aber in der Produktion tritt der neue Fehlerfall auf und wird nicht abgefangen.
Regel: Schreibe mindestens einen Integrationstest, der die echte Implementierung gegen eine Testdatenbank oder einen lokalen Mock-Server ausführt. Fake-Tests prüfen deine Logik; Integrationstests prüfen das Zusammenspiel beider Seiten. Beide Arten gehören in die CI-Pipeline – das Android-CI-Leitfaden empfiehlt genau diese Kombination für stabile Release-Builds.
Verbindung zu Play-Release-Tracks
Auf Release-Ebene zahlt sich die saubere Trennung ebenfalls aus. Interne Test-Tracks auf Google Play können mit Builds ausgeliefert werden, die zusätzliche Fake-Datenquellen oder Debug-Flags aktivieren. So bleibt der Produktionspfad unberührt, während das QA-Team gezielt Szenarien durchspielen kann, die in echten Nutzerdaten schwer reproduzierbar sind – etwa ein leeres Repository oder einen simulierten Netzwerkausfall.
Fazit
Test Dependency Replacement ist kein Trick, sondern die logische Konsequenz guter Architektur. Wenn dein Code gegen Interfaces gebunden ist und du Hilt oder eine andere DI-Lösung nutzt, ist der Austausch einer Abhängigkeit im Test ein einzeiliger Eingriff in die Testkonfiguration. Nimm dir jetzt ein Repository, das du bisher nur mit echten Daten getestet hast, extrahiere ein Interface und schreibe einen ersten Fake – inklusive eines shouldThrowError-Flags für den Fehlerpfad. Vergleiche danach deine Unit-Tests mit dem bestehenden Integrationstest: Welche Fehler findet der Fake allein, welche erst der echte Netzwerkaufruf? Diese Gegenüberstellung schärft das Gespür dafür, wie weit Fakes tragen und wo echte Integrationstests unverzichtbar bleiben.