Navigation testen

Es ist wichtig, die Navigationslogik Ihrer App vor dem Versand zu testen, um zu überprüfen, ob Ihre App wie erwartet funktioniert.

Die Navigationskomponente übernimmt die Navigation zwischen Zielen, die Übergabe von Argumenten und die Arbeit mit dem FragmentManager. Diese Funktionen wurden bereits gründlich getestet, sodass sie in Ihrer App nicht noch einmal getestet werden müssen. Wichtig sind jedoch die Interaktionen zwischen dem anwendungsspezifischen Code in den Fragmenten und deren NavController. In diesem Leitfaden werden einige gängige Navigationsszenarien und deren Test beschrieben.

Fragmentnavigation testen

Um Interaktionen von Fragmenten mit NavController isoliert zu testen, bietet Navigation 2.3 und höher ein TestNavHostController, das APIs zum Festlegen des aktuellen Ziels und zum Prüfen des Backstacks nach NavController.navigate()-Vorgängen bereitstellt.

Sie können Ihrem Projekt das Navigation Testing-Artefakt hinzufügen, indem Sie die folgende Abhängigkeit in die build.gradle-Datei Ihres App-Moduls einfügen:

Groovy

dependencies {
  def nav_version = "2.7.7"

  androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
}

Kotlin

dependencies {
  val nav_version = "2.7.7"

  androidTestImplementation("androidx.navigation:navigation-testing:$nav_version")
}

Angenommen, Sie entwickeln ein Quizspiel. Das Spiel beginnt mit einem title_screen und navigiert zu einem in_game-Bildschirm, wenn der Nutzer auf „Spielen“ klickt.

Das Fragment, das title_screen darstellt, könnte in etwa so aussehen:

Kotlin

class TitleScreen : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ) = inflater.inflate(R.layout.fragment_title_screen, container, false)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        view.findViewById<Button>(R.id.play_btn).setOnClickListener {
            view.findNavController().navigate(R.id.action_title_screen_to_in_game)
        }
    }
}

Java

public class TitleScreen extends Fragment {

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
            @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_title_screen, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        view.findViewById(R.id.play_btn).setOnClickListener(v -> {
            Navigation.findNavController(view).navigate(R.id.action_title_screen_to_in_game);
        });
    }
}

Um zu testen, ob die App den Nutzer korrekt zum in_game-Bildschirm weiterleitet, wenn er auf Play klickt, muss im Test geprüft werden, ob dieses Fragment NavController korrekt zum R.id.in_game-Bildschirm verschiebt.

Mit einer Kombination aus FragmentScenario, Espresso und TestNavHostController können Sie die Bedingungen zum Testen dieses Szenarios neu erstellen, wie im folgenden Beispiel gezeigt:

Kotlin

@RunWith(AndroidJUnit4::class)
class TitleScreenTest {

    @Test
    fun testNavigationToInGameScreen() {
        // Create a TestNavHostController
        val navController = TestNavHostController(
            ApplicationProvider.getApplicationContext())

        // Create a graphical FragmentScenario for the TitleScreen
        val titleScenario = launchFragmentInContainer<TitleScreen>()

        titleScenario.onFragment { fragment ->
            // Set the graph on the TestNavHostController
            navController.setGraph(R.navigation.trivia)

            // Make the NavController available via the findNavController() APIs
            Navigation.setViewNavController(fragment.requireView(), navController)
        }

        // Verify that performing a click changes the NavController’s state
        onView(ViewMatchers.withId(R.id.play_btn)).perform(ViewActions.click())
        assertThat(navController.currentDestination?.id).isEqualTo(R.id.in_game)
    }
}

Java

@RunWith(AndroidJUnit4.class)
public class TitleScreenTestJava {

    @Test
    public void testNavigationToInGameScreen() {

        // Create a TestNavHostController
        TestNavHostController navController = new TestNavHostController(
            ApplicationProvider.getApplicationContext());

        // Create a graphical FragmentScenario for the TitleScreen
        FragmentScenario<TitleScreen> titleScenario = FragmentScenario.launchInContainer(TitleScreen.class);

        titleScenario.onFragment(fragment ->
                // Set the graph on the TestNavHostController
                navController.setGraph(R.navigation.trivia);

                // Make the NavController available via the findNavController() APIs
                Navigation.setViewNavController(fragment.requireView(), navController)
        );

        // Verify that performing a click changes the NavController’s state
        onView(ViewMatchers.withId(R.id.play_btn)).perform(ViewActions.click());
        assertThat(navController.currentDestination.id).isEqualTo(R.id.in_game);
    }
}

Im obigen Beispiel wird eine Instanz von TestNavHostController erstellt und dem Fragment zugewiesen. Es verwendet dann Espresso, um die Benutzeroberfläche zu steuern, und prüft, ob die richtige Navigationsaktion durchgeführt wird.

Genau wie bei einem echten NavController müssen Sie setGraph aufrufen, um TestNavHostController zu initialisieren. In diesem Beispiel war das getestete Fragment das Startziel der Grafik. TestNavHostController bietet die Methode setCurrentDestination, mit der Sie das aktuelle Ziel (und optional Argumente für dieses Ziel) festlegen können, damit NavController vor Beginn des Tests den richtigen Status hat.

Im Gegensatz zu einer NavHostController-Instanz, die von einem NavHostFragment verwendet wird, löst TestNavHostController nicht das zugrunde liegende navigate()-Verhalten (wie das FragmentTransaction-Objekt von FragmentNavigator) aus, wenn Sie navigate() aufrufen. Es aktualisiert lediglich den Status von TestNavHostController.

NavigationUI mit FragmentSzenario testen

Im vorherigen Beispiel wird der für titleScenario.onFragment() bereitgestellte Callback aufgerufen, nachdem das Fragment über seinen Lebenszyklus in den Status RESUMED verschoben wurde. Zu diesem Zeitpunkt wurde die Ansicht des Fragments bereits erstellt und angehängt. Daher kann es im Lebenszyklus zu spät sein, um ordnungsgemäß zu testen. Wenn Sie beispielsweise NavigationUI mit Ansichten im Fragment verwenden, z. B. mit einem Toolbar, das vom Fragment gesteuert wird, können Sie mit dem NavController Einrichtungsmethoden aufrufen, bevor das Fragment den Status RESUMED erreicht. Daher müssen Sie TestNavHostController früher im Lebenszyklus festlegen.

Ein Fragment, das eine eigene Toolbar besitzt, kann so geschrieben werden:

Kotlin

class TitleScreen : Fragment(R.layout.fragment_title_screen) {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val navController = view.findNavController()
        view.findViewById<Toolbar>(R.id.toolbar).setupWithNavController(navController)
    }
}

Java

public class TitleScreen extends Fragment {
    public TitleScreen() {
        super(R.layout.fragment_title_screen);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        NavController navController = Navigation.findNavController(view);
        view.findViewById(R.id.toolbar).setupWithNavController(navController);
    }
}

Hier benötigen wir das NavController, das zum Zeitpunkt des onViewCreated()-Aufrufs erstellt wurde. Mit dem vorherigen Ansatz von onFragment() würde unser TestNavHostController zu spät im Lebenszyklus gesetzt werden, wodurch der findNavController()-Aufruf fehlschlägt.

FragmentScenario bietet eine FragmentFactory-Schnittstelle, mit der Callbacks für Lebenszyklusereignisse registriert werden können. Dies kann mit Fragment.getViewLifecycleOwnerLiveData() kombiniert werden, um einen Callback zu erhalten, der direkt auf onCreateView() folgt, wie im folgenden Beispiel gezeigt:

Kotlin

val scenario = launchFragmentInContainer {
    TitleScreen().also { fragment ->

        // In addition to returning a new instance of our Fragment,
        // get a callback whenever the fragment’s view is created
        // or destroyed so that we can set the NavController
        fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
            if (viewLifecycleOwner != null) {
                // The fragment’s view has just been created
                navController.setGraph(R.navigation.trivia)
                Navigation.setViewNavController(fragment.requireView(), navController)
            }
        }
    }
}

Java

FragmentScenario<TitleScreen> scenario =
FragmentScenario.launchInContainer(
       TitleScreen.class, null, new FragmentFactory() {
    @NonNull
    @Override
    public Fragment instantiate(@NonNull ClassLoader classLoader,
            @NonNull String className,
            @Nullable Bundle args) {
        TitleScreen titleScreen = new TitleScreen();

        // In addition to returning a new instance of our fragment,
        // get a callback whenever the fragment’s view is created
        // or destroyed so that we can set the NavController
        titleScreen.getViewLifecycleOwnerLiveData().observeForever(new Observer<LifecycleOwner>() {
            @Override
            public void onChanged(LifecycleOwner viewLifecycleOwner) {

                // The fragment’s view has just been created
                if (viewLifecycleOwner != null) {
                    navController.setGraph(R.navigation.trivia);
                    Navigation.setViewNavController(titleScreen.requireView(), navController);
                }

            }
        });
        return titleScreen;
    }
});

Mit dieser Technik ist das NavController verfügbar, bevor onViewCreated() aufgerufen wird. So kann das Fragment NavigationUI-Methoden verwenden, ohne dass ein Absturz entsteht.

Interaktionen mit Back-Stack-Einträgen testen

Bei der Interaktion mit den Back-Stack-Einträgen können Sie mit dem TestNavHostController den Controller über die von NavHostController übernommenen APIs mit Ihren eigenen Test-LifecycleOwner, ViewModelStore und OnBackPressedDispatcher verbinden.

Wenn Sie beispielsweise ein Fragment testen, das eine ViewModel auf Navigationsebene verwendet, müssen Sie setViewModelStore für TestNavHostController aufrufen:

Kotlin

val navController = TestNavHostController(ApplicationProvider.getApplicationContext())

// This allows fragments to use by navGraphViewModels()
navController.setViewModelStore(ViewModelStore())

Java

TestNavHostController navController = new TestNavHostController(ApplicationProvider.getApplicationContext());

// This allows fragments to use new ViewModelProvider() with a NavBackStackEntry
navController.setViewModelStore(new ViewModelStore())
  • Instrumentierte Einheitentests erstellen – Hier erfahren Sie, wie Sie Ihre instrumentierte Testsuite einrichten und Tests auf einem Android-Gerät ausführen.
  • Espresso – Testen Sie die Benutzeroberfläche Ihrer App mit Espresso.
  • JUnit4-Regeln mit AndroidX-Test: Verwenden Sie JUnit 4-Regeln mit den AndroidX-Testbibliotheken, um mehr Flexibilität zu bieten und den bei Tests erforderlichen Boilerplate-Code zu reduzieren.
  • Fragmente Ihrer App testen: Hier erfahren Sie, wie Sie Ihre App-Fragmente isoliert mit FragmentScenario testen.
  • Projekt für AndroidX Test einrichten: Hier erfährst du, wie du erforderliche Bibliotheken in den Projektdateien deiner App für die Verwendung von AndroidX Test deklarieren kannst.