Test navigation
Stay organized with collections Save and categorize content based on your preferences.

It is important to test your app's navigation logic before you ship in order to verify that your application works as you expect.

The Navigation component handles all the work of managing navigation between destinations, passing arguments, and working with the FragmentManager. These capabilities are already rigorously tested, so there is no need to test them again in your app. What is important to test, however, are the interactions between the app specific code in your fragments and their NavController. This guide walks through a few common navigation scenarios and how to test them.

Test fragment navigation

To test fragment interactions with their NavController in isolation, Navigation 2.3 and higher provides a TestNavHostController that provides APIs for setting the current destination and verify the back stack after NavController.navigate() operations.

You can add the Navigation Testing artifact to your project by adding the following dependency in your app module's build.gradle file:

Groovy

dependencies{
defnav_version="2.8.9"
androidTestImplementation"androidx.navigation:navigation-testing:$nav_version"
}

Kotlin

dependencies{
valnav_version="2.8.9"
androidTestImplementation("androidx.navigation:navigation-testing:$nav_version")
}

Let’s say you are building a trivia game. The game starts with a title_screen and navigates to an in_game screen when the user clicks play.

The fragment representing the title_screen might look something like this:

Kotlin

classTitleScreen:Fragment(){
overridefunonCreateView(
inflater:LayoutInflater,
container:ViewGroup?,
savedInstanceState:Bundle?
)=inflater.inflate(R.layout.fragment_title_screen,container,false)
overridefunonViewCreated(view:View,savedInstanceState:Bundle?){
view.findViewById<Button>(R.id.play_btn).setOnClickListener{
view.findNavController().navigate(R.id.action_title_screen_to_in_game)
}
}
}

Java

publicclass TitleScreenextendsFragment{
@Nullable
@Override
publicViewonCreateView(@NonNullLayoutInflaterinflater,
@NullableViewGroupcontainer,@NullableBundlesavedInstanceState){
returninflater.inflate(R.layout.fragment_title_screen,container,false);
}
@Override
publicvoidonViewCreated(@NonNullViewview,@NullableBundlesavedInstanceState){
view.findViewById(R.id.play_btn).setOnClickListener(v->{
Navigation.findNavController(view).navigate(R.id.action_title_screen_to_in_game);
});
}
}

To test that the app properly navigates the user to the in_game screen when the user clicks Play, your test needs to verify that this fragment correctly moves the NavController to the R.id.in_game screen.

Using a combination of FragmentScenario, Espresso, and TestNavHostController, you can recreate the conditions necessary to test this scenario, as shown in the following example:

Kotlin

@RunWith(AndroidJUnit4::class)
classTitleScreenTest{
@Test
funtestNavigationToInGameScreen(){
// Create a TestNavHostController
valnavController=TestNavHostController(
ApplicationProvider.getApplicationContext())
// Create a graphical FragmentScenario for the TitleScreen
valtitleScenario=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)
publicclass TitleScreenTestJava{
@Test
publicvoidtestNavigationToInGameScreen(){
// Create a TestNavHostController
TestNavHostControllernavController=newTestNavHostController(
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);
}
}

The above example creates an instance of TestNavHostController and assigns it to the fragment. It then uses Espresso to drive the UI and verifies that the appropriate navigation action is taken.

Just like a real NavController, you must call setGraph to initialize the TestNavHostController. In this example, the fragment being tested was the start destination of our graph. TestNavHostController provides a setCurrentDestination method that allows you to set the current destination (and optionally, arguments for that destination) so that the NavController is in the correct state before your test begins.

Unlike a NavHostController instance that a NavHostFragment would use, TestNavHostController does not trigger the underlying navigate() behavior (such as the FragmentTransaction that FragmentNavigator does) when you call navigate() - it only updates the state of the TestNavHostController.

Test NavigationUI with FragmentScenario

In the previous example, the callback provided to titleScenario.onFragment() is called after the fragment has moved through its lifecycle to the RESUMED state. By this time, the fragment’s view has already been created and attached, so it may be too late in the lifecycle to test properly. For example, when using NavigationUI with views in your fragment, such as with a Toolbar controlled by your fragment, you can call setup methods with your NavController before the fragment reaches the RESUMED state. Thus, you need a way to to set your TestNavHostController earlier in the lifecycle.

A fragment that owns its own Toolbar can be written as follows:

Kotlin

classTitleScreen:Fragment(R.layout.fragment_title_screen){
overridefunonViewCreated(view:View,savedInstanceState:Bundle?){
valnavController=view.findNavController()
view.findViewById<Toolbar>(R.id.toolbar).setupWithNavController(navController)
}
}

Java

publicclass TitleScreenextendsFragment{
publicTitleScreen(){
super(R.layout.fragment_title_screen);
}
@Override
publicvoidonViewCreated(@NonNullViewview,@NullableBundlesavedInstanceState){
NavControllernavController=Navigation.findNavController(view);
view.findViewById(R.id.toolbar).setupWithNavController(navController);
}
}

Here we need the NavController created by the time onViewCreated() is called. Using the previous approach of onFragment() would set our TestNavHostController too late in the lifecycle, causing the findNavController() call to fail.

FragmentScenario offers a FragmentFactory interface which can be used to register callbacks for lifecycle events. This can be combined with Fragment.getViewLifecycleOwnerLiveData() to receive a callback that immediately follows onCreateView(), as shown in the following example:

Kotlin

valscenario=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,newFragmentFactory(){
@NonNull
@Override
publicFragmentinstantiate(@NonNullClassLoaderclassLoader,
@NonNullStringclassName,
@NullableBundleargs){
TitleScreentitleScreen=newTitleScreen();
// 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(newObserver<LifecycleOwner>(){
@Override
publicvoidonChanged(LifecycleOwnerviewLifecycleOwner){
// The fragment’s view has just been created
if(viewLifecycleOwner!=null){
navController.setGraph(R.navigation.trivia);
Navigation.setViewNavController(titleScreen.requireView(),navController);
}
}
});
returntitleScreen;
}
});

By using this technique, the NavController is available before onViewCreated() is called, allowing the fragment to use NavigationUI methods without crashing.

Testing interactions with back stack entries

When interacting with the back stack entries, the TestNavHostController allows you to connect the controller to your own test LifecycleOwner, ViewModelStore, and OnBackPressedDispatcher by using the APIs it inherits from NavHostController.

For example, when testing a fragment that uses a navigation scoped ViewModel, you must call setViewModelStore on the TestNavHostController:

Kotlin

valnavController=TestNavHostController(ApplicationProvider.getApplicationContext())
// This allows fragments to use by navGraphViewModels()
navController.setViewModelStore(ViewModelStore())

Java

TestNavHostControllernavController=newTestNavHostController(ApplicationProvider.getApplicationContext());
// This allows fragments to use new ViewModelProvider() with a NavBackStackEntry
navController.setViewModelStore(newViewModelStore())

Content and code samples on this page are subject to the licenses described in the Content License. Java and OpenJDK are trademarks or registered trademarks of Oracle and/or its affiliates.

Last updated 2025年04月09日 UTC.