Il est important de tester la logique de navigation de votre application avant de la distribuer afin de vérifier qu'elle fonctionne comme prévu.
Le composant Navigation gère l'ensemble des tâches de gestion de la navigation entre les destinations, de transmission d'arguments et d'utilisation de FragmentManager
.
Ces fonctionnalités sont déjà soumises à des tests rigoureux. Vous n'avez donc pas besoin de les tester à nouveau dans votre application. Toutefois, il est important de tester les interactions entre le code spécifique de l'application dans vos fragments et leur NavController
.
Ce guide décrit quelques scénarios de navigation courants et explique comment les tester.
Tester la navigation par fragment
Pour tester de façon isolée les interactions des fragments avec leur NavController
, la version 2.3 du composant Navigation (et les versions ultérieures) propose un TestNavHostController
qui fournit des API permettant de définir la destination actuelle et de vérifier la pile "Retour" après les opérations NavController.navigate()
.
Vous pouvez ajouter l'artefact de test de navigation à votre projet en ajoutant la dépendance suivante dans le fichier build.gradle
de votre module d'application :
Groovy
dependencies { def nav_version = "2.8.0" androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" }
Kotlin
dependencies { val nav_version = "2.8.0" androidTestImplementation("androidx.navigation:navigation-testing:$nav_version") }
Imaginons que vous créiez un jeu de culture générale. Le jeu commence par un écran title_screen, puis passe à un écran in_game lorsque l'utilisateur clique sur le bouton "Play" (Jouer).
Le fragment représentant l'écran title_screen peut se présenter comme suit :
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); }); } }
Pour vérifier que l'application dirige correctement l'utilisateur vers l'écran in_game lorsqu'il clique sur Play (Jouer), votre test doit vérifier que ce fragment déplace correctement le NavController
sur l'écran R.id.in_game
.
En combinant FragmentScenario
, Espresso et TestNavHostController
, vous pouvez recréer les conditions nécessaires pour tester ce scénario, comme illustré dans l'exemple suivant :
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); } }
L'exemple ci-dessus crée une instance de TestNavHostController
et l'attribue au fragment. Il utilise ensuite Espresso pour piloter l'interface utilisateur et vérifie que l'action de navigation appropriée est effectuée.
Comme pour un vrai NavController
, vous devez appeler setGraph
pour initialiser le TestNavHostController
. Dans cet exemple, le fragment testé était la destination de départ de notre graphique. TestNavHostController
fournit une méthode setCurrentDestination
qui vous permet de définir la destination actuelle (et éventuellement des arguments pour cette destination) afin que l'état du NavController
soit correct avant le début du test.
Contrairement à une instance NavHostController
utilisée par un NavHostFragment
, TestNavHostController
ne peut pas déclencher le comportement de navigate()
qui en découle (contrairement au FragmentNavigator
qui le peut pour le FragmentTransaction
) lorsque vous appelez navigate()
. Il ne peut que mettre à jour l'état du TestNavHostController
.
Tester NavigationUI avec FragmentScenario
Dans l'exemple précédent, le rappel fourni à titleScenario.onFragment()
est appelé une fois que le fragment a progressé dans son cycle de vie jusqu'à l'état RESUMED
. À ce stade, la vue du fragment a déjà été créée et associée. Il est donc peut-être trop tard dans le cycle de vie pour effectuer un test correct. Par exemple, lorsque vous utilisez NavigationUI
avec des vues dans votre fragment, par exemple avec un Toolbar
contrôlé par votre fragment, vous pouvez appeler des méthodes de configuration avec votre NavController
avant que le fragment atteigne l'état RESUMED
. Vous avez donc besoin d'un moyen de définir votre TestNavHostController
plus tôt dans le cycle de vie.
Un fragment possédant son propre Toolbar
peut être écrit comme suit :
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); } }
Nous avons besoin ici du NavController
créé au moment où onViewCreated()
est appelé.
L'utilisation de l'approche précédente basée sur onFragment()
définirait notre TestNavHostController
trop tardivement dans le cycle de vie, ce qui entraînerait l'échec de l'appel de findNavController()
.
FragmentScenario
propose une interface FragmentFactory
permettant d'enregistrer des rappels pour les événements de cycle de vie. Elle peut être combinée à Fragment.getViewLifecycleOwnerLiveData()
pour recevoir un rappel qui suit immédiatement onCreateView()
, comme illustré dans l'exemple suivant :
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; } });
En utilisant cette technique, le NavController
est disponible avant l'appel de onViewCreated()
, ce qui permet au fragment d'utiliser les méthodes NavigationUI
sans plantage.
Tester les interactions avec les entrées de la pile "Retour"
En cas d'interactions avec les entrées de la pile "Retour", TestNavHostController
vous permet de connecter le contrôleur à votre propre test LifecycleOwner
, ViewModelStore
et OnBackPressedDispatcher
en utilisant les API dont il hérite de NavHostController
.
Par exemple, lorsque vous testez un fragment qui utilise un ViewModel dont la portée est limitée à la navigation, vous devez appeler setViewModelStore
sur le TestNavHostController
:
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())
Articles associés
- Créer des tests unitaires d'instrumentation : découvrez comment configurer votre suite de tests d'instrumentation et exécuter des tests sur un appareil Android.
- Espresso : testez l'UI de votre application avec Espresso.
- Règles JUnit4 avec AndroidX Test : utilisez les règles JUnit4 avec les bibliothèques AndroidX Test pour plus de flexibilité et réduire le code récurrent requis par les tests.
- Tester les fragments de votre application : découvrez comment tester les fragments de votre application de manière isolée avec
FragmentScenario
. - Configurer un projet pour AndroidX Test : découvrez comment déclarer des bibliothèques requises dans les fichiers de projet de votre application pour utiliser AndroidX Test.