Android Coden
Android 4 min lesen

Fakes statt Mocks

Fakes ersetzen brüchige Mocks durch realistische In-Memory-Implementierungen. Du erfährst, wann dieser Tausch die Testwartbarkeit messbar verbessert.

Wer Android-Apps testet, stößt früher oder später auf eine Weggabelung: Mock oder Fake? Viele Teams greifen reflexartig zum Mock-Framework und landen dann in einer Wartungsfalle – Tests, die bei jedem Refactoring rot werden, obwohl das beobachtbare Verhalten der App unverändert ist. Die Faustregel „Fakes statt Mocks” gibt dir eine klare Strategie an die Hand, mit der du robuste, vertrauenswürdige Tests schreibst, die wirklich etwas über deine App aussagen.

Was ist das?

Ein Fake ist eine vereinfachte, aber echte Implementierung eines Interfaces – typischerweise vollständig im Speicher. Im Gegensatz zu einem Mock, den du mit Mockito oder MockK konfigurierst, besitzt ein Fake wirklich Zustand. Er speichert Daten, gibt sie korrekt zurück und verhält sich so, wie eine echte Implementierung es tun würde – nur ohne Datenbank, Netzwerk oder Dateisystem.

Ein Mock hingegen ist ein Stellvertreter, dessen Rückgabewerte du vorab festlegst (every { ... } returns ...). Das Problem: Mocks prüfen häufig wie etwas aufgerufen wird (Interaktionen), nicht was dabei herauskommt (Verhalten). Sobald du interne Implementierungsdetails refaktorierst, schlagen diese Tests fehl – obwohl deine App noch korrekt funktioniert.

Im Android-Kontext tritt das klassisch bei Repositories auf. Du hast ein UserRepository-Interface, das dein ViewModel oder dein Use Case konsumiert. Mit einem Fake simulierst du dieses Interface vollständig, ohne ein Mock-Framework zu bemühen, und du prüfst tatsächliches Verhalten statt vorgetäuschter Rückgabewerte.

Wie funktioniert es?

Der Kern ist simpel: Du erstellst eine Klasse, die dein Interface implementiert, und speicherst den Zustand in einer MutableList oder MutableMap. Dependency Injection – ob mit Hilt, Koin oder manuell – sorgt dafür, dass dein echter Code die echte Implementierung bekommt, während dein Test die Fake-Variante injiziert.

Ein typisches Muster in einem Android-Projekt:

interface UserRepository {
    suspend fun getUser(id: String): User?
    suspend fun saveUser(user: User)
}

class FakeUserRepository : UserRepository {
    private val users = mutableMapOf<String, User>()

    override suspend fun getUser(id: String): User? = users[id]

    override suspend fun saveUser(user: User) {
        users[user.id] = user
    }
}

Kein every { ... }, kein verify { ... }. Die Fake-Klasse lebt in deinem test- oder androidTest-Ordner und kann von allen Tests wiederverwendet werden. Da Fakes echten Zustand halten, lassen sich auch komplexe Szenarien abbilden: Schreib einen User in einem Testschritt, lies ihn in einem anderen – genau so, wie es die echte App tun würde. Das gibt deinen Tests eine Aussagekraft, die ein Mock nicht erreichen kann.

In der Praxis

Nehmen wir ein ViewModel, das einen User lädt und in einem UI-State bereitstellt:

class ProfileViewModel(
    private val userRepository: UserRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow<ProfileUiState>(ProfileUiState.Loading)
    val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()

    fun loadUser(id: String) {
        viewModelScope.launch {
            val user = userRepository.getUser(id)
            _uiState.value = if (user != null) {
                ProfileUiState.Success(user)
            } else {
                ProfileUiState.NotFound
            }
        }
    }
}

Mit dem FakeUserRepository sieht der passende Test so aus:

@Test
fun `loadUser emits Success when user exists`() = runTest {
    val fakeRepo = FakeUserRepository()
    fakeRepo.saveUser(User(id = "42", name = "Ada"))

    val viewModel = ProfileViewModel(fakeRepo)
    viewModel.loadUser("42")

    val state = viewModel.uiState.value
    assertIs<ProfileUiState.Success>(state)
    assertEquals("Ada", state.user.name)
}

Kein Mocking-Overhead, kein Aufsetzen von Stub-Ketten. Der Test liest sich wie eine Beschreibung des Verhaltens – und das ist kein Zufall, sondern das Ziel.

Typische Stolperfalle: Mock-Drift

Ein verbreiteter Fehler ist das sogenannte Mock-Drift: Du refaktorierst dein Repository – zum Beispiel änderst du eine private Hilfsmethode oder die interne Aufruf-Reihenfolge – und plötzlich schlagen zehn Tests fehl, weil Mockito die alten Interaktionen verifiziert. Mit einem Fake passiert das nicht. Solange das Interface stabil ist, laufen alle Tests durch, egal wie du die Implementierung umbaust. Das ist der entscheidende Gewinn an Maintainability: Deine Tests sind gegen das Interface geschrieben, nicht gegen die Implementierung dahinter. Änderst du das Interface selbst, zeigt der Compiler dir sofort alle Stellen – im Produktionscode und im Fake gleichermaßen.

Fazit

Fakes sind kein Allheilmittel, aber sie sind das richtige Werkzeug, sobald dein Repository oder Service komplexes Zustandsverhalten besitzt, das sich mit Stubs schlecht ausdrücken lässt. Prüfe in deinem aktuellen Projekt: Gibt es Tests, die bei einem Refactoring regelmäßig rot werden, obwohl die App korrekt läuft? Das ist ein klares Signal. Schreib einen FakeUserRepository, migriere einen bestehenden Test darauf, und vergleiche Lesbarkeit und Robustheit direkt – der Unterschied ist sofort spürbar. Wer diesen Schritt einmal gemacht hat, greift beim nächsten Interface selten noch reflexartig zum Mock-Framework.

Quellen (2)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.