Android Coden
Android 7 min lesen

Data Layer Testing in Android

Teste Repositories ohne echte Server. So prüfst du Datenlogik mit Fake-APIs und In-Memory-Datenbanken.

Data Layer Testing bedeutet: Du prüfst das Verhalten deiner Datenschicht kontrolliert und wiederholbar, ohne dich auf echte Netzwerke, echte Serverantworten oder produktive Datenbanken zu verlassen. Gerade in modernen Android-Apps mit Kotlin, Jetpack, Compose und klarer Architektur ist das wichtig, weil die UI nur dann verlässlich reagieren kann, wenn Repositories, lokale Datenquellen und entfernte APIs saubere Zustände liefern.

Was ist das?

Data Layer Testing ist das Testen der Schicht, die Daten lädt, speichert, kombiniert und an andere App-Schichten weitergibt. In einer typischen Android-Architektur besteht diese Schicht aus Repositories, API-Clients, Datenbank-DAOs, Mappern und manchmal Synchronisationslogik für Offline-First-Verhalten. Du testest hier nicht, ob ein Button in Compose sichtbar ist. Du testest, ob UserRepository bei erfolgreichem API-Aufruf die richtigen Daten liefert, ob es bei einem Netzwerkfehler aus der lokalen Datenbank liest oder ob es fehlerhafte Daten korrekt ablehnt.

Das mentale Modell ist einfach zu greifen: Die Datenschicht ist ein Vertrag. Andere Teile deiner App, etwa ViewModels oder Use Cases, verlassen sich darauf, dass dieser Vertrag eingehalten wird. Wenn getArticles() einen Flow<List<Article>> zurückgibt, muss klar sein, wann Werte kommen, was bei Fehlern passiert und aus welcher Quelle die Daten stammen. Ein Test macht diesen Vertrag sichtbar.

Der wichtigste Rahmen lautet: Teste Datenverhalten ohne echte Dienste. Ein echter Server kann langsam sein, offline sein, andere Daten liefern oder Rate Limits haben. Eine echte Datenbankdatei kann alten Zustand enthalten. Solche Abhängigkeiten machen Tests unzuverlässig. Deshalb nutzt du Fake-APIs, In-Memory-Datenbanken und gezielt austauschbare Repositories. Damit steuerst du exakt, welche Antwort ein Test bekommt.

In der täglichen Android-Entwicklung taucht das Thema früh auf, sobald du nicht mehr nur einzelne Funktionen schreibst. Eine App zeigt vielleicht Aufgaben, Nachrichten, Profile oder Warenkörbe an. Diese Daten kommen aus mehreren Quellen. Die UI sollte trotzdem stabil bleiben. Gute Data-Layer-Tests helfen dir, Fehler zu finden, bevor sie als leere Listen, falsche Ladezustände oder kaputte Offline-Szenarien in der App sichtbar werden.

Wie funktioniert es?

Der Kern ist Trennung. Deine produktive App spricht vielleicht mit Retrofit, Room und einem echten Backend. Dein Test sollte dieselbe Repository-Logik ausführen, aber kontrollierte Testquellen verwenden. Dafür brauchst du Abhängigkeiten, die austauschbar sind. Statt ein Repository intern selbst einen API-Client erstellen zu lassen, bekommt es ein Interface oder eine Datenquelle per Konstruktor. So kann der Test eine Fake-Implementierung einsetzen.

Eine Fake-API ist eine kleine Testimplementierung, die sich wie eine entfernte Datenquelle verhält. Sie liefert zum Beispiel eine Liste von DTOs zurück, wirft absichtlich eine Exception oder zählt, wie oft sie aufgerufen wurde. Wichtig ist: Ein Fake ist nicht dasselbe wie ein Mock. Ein Mock prüft häufig einzelne Aufrufe. Ein Fake bildet ein vereinfachtes, aber nutzbares Verhalten nach. Für Lernende ist ein Fake oft verständlicher, weil du ihn wie eine kleine Testversion deines Backends lesen kannst.

Eine In-Memory-Datenbank ist eine Datenbank, die nur während des Tests existiert. Bei Room kannst du eine Datenbank im Arbeitsspeicher erstellen. Sie ist schnell, enthält keine alten Daten und verschwindet nach dem Test. Damit testest du echte SQL-Abfragen, echte DAO-Methoden und echte Entity-Mappings, ohne eine Datei auf dem Gerät oder Emulator dauerhaft zu verändern. Das ist besonders wertvoll, wenn dein Repository lokale Daten als Quelle der Wahrheit nutzt.

Repositories sind meist die beste Testgrenze. Sie kapseln die Entscheidung, ob Daten aus dem Netzwerk, aus der Datenbank oder aus beiden Quellen kommen. In einer Offline-First-App kann ein Repository zuerst lokale Daten als Flow liefern und parallel versuchen, neue Daten vom Server zu holen. Ein Test kann dann prüfen: Werden lokale Daten sofort geliefert? Wird nach erfolgreicher Synchronisation die Datenbank aktualisiert? Bleibt bei einem Netzwerkfehler der vorhandene lokale Zustand nutzbar?

Für Kotlin und Coroutines brauchst du zusätzlich Kontrolle über Nebenläufigkeit. Wenn dein Repository suspend-Funktionen oder Flow nutzt, sollten Tests deterministisch laufen. Du verwendest dafür Test-Coroutine-Werkzeuge wie runTest und testbare Dispatcher. So vermeidest du Tests, die nur manchmal fehlschlagen, weil ein Hintergrundjob noch nicht fertig war.

Ein guter Data-Layer-Test folgt meistens diesem Ablauf: Zustand vorbereiten, Aktion ausführen, Ergebnis prüfen. Das klingt banal, ist aber entscheidend. Du befüllst zum Beispiel eine In-Memory-Datenbank, konfigurierst eine Fake-API, rufst das Repository auf und prüfst danach Rückgabewert oder Datenbankzustand. Der Test sollte beschreiben, welches Verhalten erwartet wird. Er sollte nicht jede interne Hilfsfunktion absichern, denn dann bricht er bei harmlosen Refactorings.

In der Praxis

Stell dir ein Repository vor, das Artikel aus einer entfernten Quelle lädt und lokal speichert. Die UI beobachtet später die Datenbank. Der Test soll prüfen, ob ein erfolgreicher Refresh die lokalen Daten ersetzt. Dafür brauchst du keinen echten Server. Du brauchst eine Fake-API mit kontrollierter Antwort und eine In-Memory-Datenbank.

Ein vereinfachtes Kotlin-Beispiel kann so aussehen:

interface ArticleApi {
    suspend fun fetchArticles(): List<ArticleDto>
}

class FakeArticleApi(
    var result: Result<List<ArticleDto>>
) : ArticleApi {
    override suspend fun fetchArticles(): List<ArticleDto> {
        return result.getOrThrow()
    }
}

class ArticleRepository(
    private val api: ArticleApi,
    private val dao: ArticleDao
) {
    fun observeArticles(): Flow<List<Article>> =
        dao.observeAll().map { entities ->
            entities.map { Article(id = it.id, title = it.title) }
        }

    suspend fun refresh() {
        val remoteArticles = api.fetchArticles()
        dao.replaceAll(
            remoteArticles.map { dto ->
                ArticleEntity(id = dto.id, title = dto.title)
            }
        )
    }
}

@Test
fun refresh_speichert_remote_daten_lokal() = runTest {
    val db = Room.inMemoryDatabaseBuilder(
        context,
        AppDatabase::class.java
    ).build()

    val fakeApi = FakeArticleApi(
        Result.success(
            listOf(
                ArticleDto(id = "1", title = "Kotlin testen"),
                ArticleDto(id = "2", title = "Room verstehen")
            )
        )
    )

    val repository = ArticleRepository(
        api = fakeApi,
        dao = db.articleDao()
    )

    repository.refresh()

    val gespeicherteArtikel = db.articleDao().getAllOnce()

    assertEquals(2, gespeicherteArtikel.size)
    assertEquals("Kotlin testen", gespeicherteArtikel.first().title)

    db.close()
}

Dieses Beispiel zeigt mehrere wichtige Punkte. Der Test prüft das Verhalten des Repositories, nicht Retrofit, nicht den echten Server und nicht die Compose-Oberfläche. Die Fake-API liefert exakt die Daten, die du für den Test brauchst. Die In-Memory-Datenbank erlaubt dir, echte DAO-Logik zu verwenden. Dadurch findest du auch Fehler in Queries, Transaktionen oder Mappings.

Eine sinnvolle Entscheidungsregel lautet: Teste Mapper und kleine Berechnungen mit normalen Unit-Tests, teste DAO-Verhalten mit In-Memory-Room-Tests und teste Repository-Verhalten mit Fake-APIs plus kontrollierter lokaler Datenquelle. Wenn du alles in einem einzigen riesigen Integrationstest prüfst, wird die Fehlersuche schwer. Wenn du dagegen nur Mapper testest, übersiehst du Fehler in der Zusammenarbeit der Bausteine.

Eine typische Stolperfalle ist ein Fake, der zu freundlich ist. Wenn deine Fake-API immer gültige Daten liefert, testest du nur den besten Fall. Echte Apps scheitern aber oft an leeren Antworten, doppelten IDs, Netzwerkfehlern, ungültigen Feldern oder veralteten lokalen Daten. Plane deshalb bewusst Tests für Fehlerfälle. Beispiel: Die API wirft eine Exception, aber das Repository darf vorhandene lokale Artikel nicht löschen. Genau solche Regeln schützen deine App-Qualität.

Eine zweite Stolperfalle ist zu starke Kopplung an die Implementierung. Wenn dein Test prüft, dass Methode A exakt vor Methode B aufgerufen wurde, obwohl nur das Endergebnis relevant ist, wird der Test empfindlich. Besser ist oft: Prüfe, dass nach refresh() die erwarteten Daten lokal vorhanden sind. Damit bleibt der Test wertvoll, auch wenn du intern später eine Transaktion, einen Mapper oder eine andere Datenquelle einführst.

Achte auch auf Testdaten. Verwende klare, kleine Datensätze mit sprechenden Werten. Ein Artikel mit Titel "A" hilft weniger als "Lokaler Entwurf" oder "Remote-Version", wenn du später einen fehlgeschlagenen Test liest. Für Anfänger ist das ein unterschätzter Punkt: Gute Testdaten sind Dokumentation. Sie zeigen, welches Szenario gerade gemeint ist.

Bei Offline-First-Logik solltest du besonders sorgfältig testen. Ein Repository kann lokale Daten beobachten, remote Daten laden und die Datenbank aktualisieren. Dann reicht ein einzelner Rückgabewert oft nicht. Du musst prüfen, welche Werte ein Flow nacheinander ausgibt. Zum Beispiel zuerst eine lokale Liste, später eine aktualisierte Liste. Dafür nutzt du in Tests kontrollierte Coroutine-Ausführung und sammelst Flow-Emissionen gezielt ein. Wichtig ist, dass der Test nicht auf echte Zeit wartet. Echte Wartezeiten machen Tests langsam und instabil.

In Code-Reviews solltest du bei Data-Layer-Tests auf drei Fragen achten. Erstens: Hängt der Test von Netzwerk, Uhrzeit, Dateisystemzustand oder Reihenfolge anderer Tests ab? Zweitens: Prüft er ein fachliches Datenverhalten, das für die App relevant ist? Drittens: Gibt es mindestens einen Fehlerfall neben dem Erfolgsfall? Wenn du diese Fragen sauber beantworten kannst, ist der Test meist nützlich.

Fazit

Data Layer Testing hilft dir, den Datenkern deiner Android-App verlässlich zu machen: Repositories werden gegen kontrollierte Fake-APIs, In-Memory-Datenbanken und klare Fehlerfälle geprüft, statt von echten Diensten abhängig zu sein. Übe das an einem kleinen Repository mit refresh(), lokalem Cache und einem Fehlerfall: Setze Breakpoints, beobachte die Datenbank vor und nach dem Aufruf, schreibe mindestens einen Test für Erfolg und einen für Netzwerkfehler, und prüfe im Code-Review, ob der Test wirklich Verhalten beschreibt.

Quellen (6)
Redaktion

Geschrieben von

Redaktion

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