Android Coden
Android 4 min lesen

Navigation Testing

Navigation Testing prüft, ob Nutzer sicher durch App-Flows navigieren. Routen, Back-Stack und Deep Links werden dabei systematisch verifiziert.

Navigation zwischen Screens ist eine der häufigsten Nutzeraktionen in jeder App – und gleichzeitig eine der am häufigsten untesteten. Navigation Testing stellt sicher, dass Routen korrekt aufgelöst werden, der Back-Stack bei jedem Flow seinen erwarteten Zustand hat und Deep Links aus externen Quellen an der richtigen Stelle landen. Wer diese drei Aspekte absichert, schließt eine der häufigsten Quellen für UX-Bugs in produktiven Android-Apps.

Was ist das?

Navigation Testing bezeichnet das gezielte Überprüfen des Navigationsverhaltens einer Android-App. Es geht nicht um das visuelle Aussehen einzelner Screens, sondern um die Frage: Landet der Nutzer nach einer Aktion an der richtigen Stelle? Kann er mit dem Zurück-Button dorthin zurückkehren, wo er vorher war? Öffnet ein Deep Link aus einer Push-Nachricht den richtigen Zielscreen – mit den richtigen Parametern?

Im Kontext des Jetpack Navigation Frameworks besteht Navigation aus drei Kernelementen: Routen (die Ziele), einem NavController (der die Navigation steuert) und einem NavGraph (der die möglichen Pfade definiert). Navigation Testing greift genau hier an: Es verifiziert, ob der NavController nach bestimmten Nutzeraktionen den erwarteten Zustand hält. Das ist etwas anderes als ein Snapshot-Test oder ein UI-Test, der prüft, ob ein bestimmtes Widget sichtbar ist – es geht um den Kontrollfluss selbst.

Für Lernende ist der entscheidende Gedankenwechsel: Navigation ist Logik, keine reine Darstellung. Und Logik lässt sich testen, ohne zwingend eine vollständige Activity hochfahren zu müssen.

Wie funktioniert es?

Jetpack stellt für automatisierte Navigationstests das Artifact androidx.navigation:navigation-testing bereit. Es enthält TestNavHostController, eine instrumentierte Variante des NavController, die sich in Unit- und Integrationstests einbinden lässt, ohne echte Fragment-Transaktionen oder Activity-Lebenszyklen zu benötigen.

Der typische Testaufbau für Compose-basierte Navigation folgt diesen Schritten:

  1. Einen TestNavHostController instanziieren und den NavGraph per setGraph() zuweisen.
  2. Den Controller der zu testenden Composable via CompositionLocalProvider übergeben.
  3. Eine Nutzeraktion auslösen – Klick auf einen Button, Formular absenden, Swipe.
  4. Den aktuellen Zustand prüfen: navController.currentDestination?.route muss dem erwarteten Ziel entsprechen.

Für Back-Stack-Assertions liefert navController.previousBackStackEntry?.destination?.route den direkten Vorgänger auf dem Stack. Besonders wichtig ist das bei Flows, die popUpTo mit inclusive = true nutzen – dort verschwindet eine ganze Stack-Sektion auf einen Schlag, und ein fehlender Test macht diesen Effekt unsichtbar bis zum Nutzerbericht.

Deep Links werden als vollständige Intent-Objekte an die Activity weitergegeben. Im Instrumentierungstest erzeugst du einen Intent mit dem URI des Deep Links und startest die Activity damit. Anschließend prüfst du, ob die richtige Destination aktiv ist und ob die übergebenen Parameter korrekt ankommen.

In der Praxis

Ein klassisches Szenario: Deine App hat einen Login-Flow – LoginScreen → HomeScreen. Nach erfolgreichem Login soll der Nutzer mit dem Zurück-Button nicht mehr zur Login-Seite gelangen. Dieser popUpTo-Effekt lässt sich direkt automatisiert testen:

@Test
fun loginSuccess_raeumt_backStack_auf() {
    val navController = TestNavHostController(
        ApplicationProvider.getApplicationContext()
    )

    composeTestRule.setContent {
        navController.setGraph(R.navigation.app_graph)
        CompositionLocalProvider(LocalNavController provides navController) {
            AppNavHost(navController = navController)
        }
    }

    // Annahme: Felder sind vorausgefüllt oder werden per performTextInput gesetzt
    composeTestRule.onNodeWithText("Anmelden").performClick()

    // Prüfung 1: Aktuelles Ziel ist HomeScreen
    assertThat(navController.currentDestination?.route).isEqualTo("home")

    // Prüfung 2: Kein Eintrag mehr unter HomeScreen → Login ist weg
    assertThat(navController.previousBackStackEntry).isNull()
}

Typische Stolperfalle: Viele Entwickler testen nur, ob der richtige Screen angezeigt wird, ignorieren aber den Back-Stack-Zustand. In der Produktion heißt das: Nutzer drücken auf dem HomeScreen Zurück und landen wieder im Login – ein bekannter UX-Bug, der sich im manuellen Testen leicht übersieht, weil man den Happy Path mit einem frischen Back-Stack durchläuft.

Ein zweiter häufiger Fehler betrifft Deep-Link-Parameter: Der URI wird übergeben, aber es wird nie verifiziert, ob der Parameter korrekt im Ziel-ViewModel ankommt. Kombiniere deshalb den Navigationstest mit einer State-Assertion: Prüfe das UI-Element oder den ViewModel-State, der den Parameter anzeigen soll. Nur dann ist der vollständige Deep-Link-Kanal abgesichert – nicht nur die Weiche zur richtigen Destination.

Für Routen mit optionalen Argumenten lohnt es sich, einen negativen Testfall zu schreiben: Was passiert, wenn der URI keinen erwarteten Query-Parameter enthält? Landet die App auf einem Fallback-Screen oder stürzt sie ab? Diese Grenzfälle deckt manuelle Exploration selten zuverlässig ab.

Fazit

Navigation Testing fällt erst dann auf, wenn es fehlt – beim ersten Nutzerbericht über einen Endlos-Back-Stack oder einen Deep Link, der ins Leere führt. Geh nach dem nächsten Feature-PR einen konkreten Schritt: Trace den kritischsten Flow deiner App manuell durch, schreibe dann einen automatisierten Test dafür und stelle sicher, dass der Back-Stack-Zustand nach jedem Schritt explizit geprüft wird. Wer Deep Links nutzt, ergänzt außerdem einen dedizierten Intent-Test, der den Aufruf von außen simuliert. Diese zwei Investitionen zusammen decken die häufigsten Navigationsfehler ab, bevor sie Nutzer zu Gesicht bekommen.

Quellen (4)
Redaktion

Geschrieben von

Redaktion

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