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:
Groovig
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())
Weitere Informationen
- 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.