Fragmentnavigation testen

Es ist wichtig, die Navigationslogik Ihrer App vor der Veröffentlichung zu testen, um sicherzustellen, dass Ihre Anwendung wie erwartet funktioniert.

Die Navigation-Komponente übernimmt die gesamte Arbeit für die Verwaltung der Navigation zwischen Zielen, das Übergeben von Argumenten und die Arbeit mit dem FragmentManager. Diese Funktionen werden bereits gründlich getestet, sodass Sie sie in Ihrer App nicht noch einmal testen müssen. Wichtig ist jedoch, die Interaktionen zwischen dem app-spezifischen Code in Ihren Fragmenten und ihren NavController zu testen.

Isoliert testen

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

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

Groovy

dependencies {
  def nav_version = "2.9.8"

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

Kotlin

dependencies {
  val nav_version = "2.9.8"

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

Quizspiele Das Spiel beginnt mit einem Titelbildschirm und wechselt zu einem In-Game-Bildschirm, wenn der Nutzer auf „Spielen“ klickt.

Navigationsfluss für Quizspiele
Abbildung 1. Navigationsfluss für Quizspiele

Das Fragment für den title_screen könnte 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 Bildschirm in_game weiterleitet, wenn er auf Play klickt, muss in Ihrem Test geprüft werden, ob dieses Fragment das NavController korrekt zum Bildschirm R.id.in_game verschiebt.

Mit einer Kombination aus FragmentScenario, Espresso und TestNavHostController können Sie die Bedingungen für das Testen dieses Szenarios nachbilden, 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 using 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
    fun 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 using 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 vorherigen Beispiel wird eine Instanz von TestNavHostController erstellt und dem Fragment zugewiesen. Anschließend wird Espresso verwendet, um die Benutzeroberfläche zu steuern und zu prüfen, ob die entsprechende Navigationsaktion ausgeführt wird.

Genau wie bei einem echten NavController müssen Sie setGraph aufrufen, um das TestNavHostController zu initialisieren. In diesem Beispiel war das getestete Fragment das Startziel unseres Diagramms. TestNavHostController bietet eine setCurrentDestination-Methode, mit der Sie das aktuelle Ziel (und optional Argumente für dieses Ziel) festlegen können, sodass sich NavController vor Beginn des Tests im richtigen Zustand befindet.

Im Gegensatz zu einer NavHostController-Instanz, die von einem NavHostFragment verwendet wird, löst TestNavHostController beim Aufrufen von navigate() nicht das zugrunde liegende navigate()-Verhalten aus (z. B. das FragmentTransaction, das FragmentNavigator auslöst). Es wird nur der Status des TestNavHostController aktualisiert.

NavigationUI mit FragmentScenario testen

Im vorherigen Beispiel wird der für titleScenario.onFragment() bereitgestellte Callback aufgerufen, nachdem das Fragment seinen Lebenszyklus bis zum Status RESUMED durchlaufen hat. Zu diesem Zeitpunkt wurde die Ansicht des Fragments bereits erstellt und angehängt. Es ist also möglicherweise zu spät im Lebenszyklus, um sie richtig zu testen. Wenn Sie beispielsweise NavigationUI mit Ansichten in Ihrem Fragment verwenden, z. B. mit einem Toolbar, das von Ihrem Fragment gesteuert wird, können Sie Einrichtungsmethoden mit Ihrem NavController aufrufen, bevor das Fragment den Status RESUMED erreicht. Sie benötigen also eine Möglichkeit, Ihre TestNavHostController früher im Lebenszyklus festzulegen.

Ein Fragment, das ein eigenes Toolbar hat, 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 die NavController, die bis zum Aufruf von onViewCreated() erstellt wurde. Wenn wir den vorherigen Ansatz mit onFragment() verwenden, wird TestNavHostController zu spät im Lebenszyklus festgelegt, sodass der findNavController()-Aufruf fehlschlägt.

FragmentScenario bietet eine FragmentFactory-Schnittstelle, mit der Callbacks für Lifecycle-Ereignisse registriert werden können. Dies kann mit Fragment.getViewLifecycleOwnerLiveData() kombiniert werden, um einen Callback zu erhalten, der unmittelbar 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;
    }
});

Durch diese Technik ist NavController verfügbar, bevor onViewCreated() aufgerufen wird. So kann das Fragment NavigationUI-Methoden verwenden, ohne abzustürzen.

Interaktionen mit Backstack-Einträgen testen

Bei der Interaktion mit den Einträgen im Backstack können Sie mit TestNavHostController den Controller über die APIs, die er von NavHostController erbt, mit Ihren eigenen Test-LifecycleOwner, ViewModelStore und OnBackPressedDispatcher verbinden.

Wenn Sie beispielsweise ein Fragment testen, das ein ViewModel mit Navigationsbereich 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: Mit Espresso können Sie die Benutzeroberfläche Ihrer App testen.
  • JUnit4-Regeln mit AndroidX Test: Verwenden Sie JUnit 4-Regeln mit den AndroidX Test-Bibliotheken, um mehr Flexibilität zu bieten und den in Tests erforderlichen Boilerplate-Code zu reduzieren.
  • App-Fragmente testen: Hier erfahren Sie, wie Sie die Fragmente Ihrer App mit FragmentScenario isoliert testen.
  • Projekt für AndroidX Test einrichten: Hier erfahren Sie, wie Sie die erforderlichen Bibliotheken in den Projektdateien Ihrer App deklarieren, um AndroidX Test zu verwenden.