Android Coden
Android 4 min lesen

Die Testing-Pyramide für Android

Die Testing-Pyramide hilft dir, schnelle Unit-Tests und realistische UI-Tests sinnvoll zu balancieren. Lerne, wie du dein Testbudget effizient einteilst.

Wer eine Android-App ohne automatisierte Tests ausliefert, navigiert im Blindflug. Die Testing-Pyramide ist das mentale Modell, das dir zeigt, wie viele Tests du auf welcher Ebene schreiben solltest, damit deine Suite sowohl schnell als auch aussagekräftig bleibt – und dir im CI nicht die Zeit stiehlt, die du zum Entwickeln brauchst.

Was ist das?

Die Testing-Pyramide ist ein Architektur-Leitfaden für automatisierte Tests mit drei klar abgegrenzten Schichten: Unit-Tests an der breiten Basis, Integrationstests in der Mitte und UI-Tests an der schmalen Spitze. Die Breite jeder Schicht beschreibt das ideale Mengenverhältnis – viele schnelle Unit-Tests, eine moderate Zahl an Integrationstests und wenige, gezielte UI-Tests.

Im Android-Kontext deckt sich dieses Modell direkt mit Googles offizieller Empfehlung. Unit-Tests laufen auf der JVM, brauchen kein Android-Framework und sind in Sekunden erledigt. Integrationstests überprüfen, ob einzelne Komponenten korrekt zusammenarbeiten – etwa ob dein Repository Daten richtig in Room persistiert. UI-Tests fahren einen echten Emulator oder ein Gerät hoch und validieren das Verhalten aus Nutzersicht.

Das Modell löst ein konkretes Problem: Ohne Orientierung neigen Teams dazu, fast ausschließlich UI-Tests zu schreiben, weil sie dem echten Nutzererlebnis am nächsten kommen. Das Ergebnis ist eine langsame, fragile Suite, die bei jeder Layout-Verschiebung bricht – und die niemand mehr anfassen will.

Wie funktioniert es?

Jede Schicht der Pyramide bedient sich eigener Werkzeuge im Android-Ökosystem.

Unit-Tests (Basis)

Unit-Tests liegen im test/-Verzeichnis und laufen auf der JVM. Du nutzt JUnit 4 oder JUnit 5 zusammen mit einer Mocking-Bibliothek wie Mockito oder MockK. ViewModel-Tests, Use-Case-Tests und reine Kotlin-Logik gehören hierhin. Mit Robolectric kannst du Context-abhängige Klassen testen, ohne einen Emulator zu starten – das hält die Laufzeit im einstelligen Sekundenbereich. Für Coroutines nutzt du kotlinx-coroutines-test mit runTest und advanceUntilIdle.

Integrationstests (Mitte)

Integrationstests liegen im androidTest/-Verzeichnis und laufen auf einem Gerät oder Emulator. AndroidX Test stellt ApplicationProvider und InstrumentationRegistry bereit. Für Room-Datenbanken nutzt du Room.inMemoryDatabaseBuilder, um eine echte SQLite-Instanz im isolierten Zustand zu testen. Auch Hilt-Abhängigkeitsgraphen lassen sich mit HiltAndroidRule in Integrationstests prüfen, ohne die gesamte UI hochzufahren.

UI-Tests (Spitze)

Espresso ist das klassische Framework für View-basierte UIs; für Jetpack Compose steht Compose UI Test mit composeTestRule.onNodeWithText(...) und performClick() zur Verfügung. Beide starten eine Activity und interagieren wie ein Nutzer. Diese Tests sind wertvoll für kritische User Journeys – Anmeldung, Checkout, Onboarding – sollten aber sparsam eingesetzt werden, weil sie Emulator-Zeit und Netzwerkzugang voraussetzen.

In der Praxis

Hier ist ein typisches Unit-Test-Beispiel für ein ViewModel, das Artikel aus einem gefakten Repository lädt:

@Test
fun `uiState emits loading then content`() = runTest {
    val fakeRepo = FakeArticleRepository(articles = sampleArticles)
    val viewModel = ArticleListViewModel(fakeRepo)

    val states = mutableListOf<ArticleUiState>()
    val job = launch { viewModel.uiState.collect { states.add(it) } }

    advanceUntilIdle()
    job.cancel()

    assertThat(states.first()).isInstanceOf(ArticleUiState.Loading::class.java)
    assertThat(states.last()).isInstanceOf(ArticleUiState.Content::class.java)
}

Kein Emulator, kein Netzwerk, kein Android-Framework – nur pure Kotlin-Logik, die in unter einer Sekunde durchläuft.

Stolperfalle: Invertierte Pyramide

Das sogenannte Ice-Cream-Cone-Anti-Pattern entsteht, wenn Teams wenige Unit-Tests, kaum Integrationstests, dafür aber Dutzende UI-Tests schreiben. Das Ergebnis ist ein CI-Lauf, der 30 Minuten dauert, und eine Suite, die bei einer harmlosen Layout-Änderung zwölf Tests rot markiert. Statt zu debuggen, löscht man Tests – und verliert Coverage, ohne es zu merken.

Die Faustregel lautet: 70 % Unit-Tests, 20 % Integrationstests, 10 % UI-Tests. Für eine App mit 200 Tests bedeutet das 140 JVM-Tests, 40 Instrumentierungstests und 20 Espresso- bzw. Compose-UI-Tests.

Eine zweite Falle ist das Testen von Implementierungsdetails statt von Verhalten. Wenn du prüfst, ob eine private Methode aufgerufen wurde, ist dein Test an Interna gebunden und bricht bei jedem Refactoring. Verankere Assertions stattdessen am beobachtbaren Zustand: uiState.value, dem Inhalt einer Room-Tabelle oder dem Text, den Compose auf dem Bildschirm rendert.

Fazit

Die Testing-Pyramide ist kein starres Regelwerk, sondern ein Kalibrierungs-Werkzeug. Öffne dein aktuelles Projekt und zähle, wie viele Tests in test/ und wie viele in androidTest/ liegen. Ist das Verhältnis invertiert, schreibe die nächsten drei Korrekturen bewusst als Unit-Tests, bevor du einen weiteren UI-Test anlegst. Führe ./gradlew test und ./gradlew connectedAndroidTest getrennt aus und vergleiche die Laufzeiten – der Unterschied zeigt dir sofort, wo deine Pyramide noch schief steht und wo du das größte Beschleunigungspotenzial hast.

Quellen (2)
Redaktion

Geschrieben von

Redaktion

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