Test Compose navigation

Decouple the navigation code from your composable destinations to enable testing each composable in isolation, separate from the NavHost composable.

Don't pass the NavController directly into any composable. Instead, pass navigation callbacks (lambdas) as parameters. This allows all your composables to be individually testable, as they don't require an instance of NavController in tests.

The composable lambda in your NavHost acts as a bridge between the Navigation APIs and your composable:

@Composable
fun ProfileScreen(
    userId: String,
    navigateToFriendProfile: (friendUserId: String) -> Unit
) {
 // …
}

// In your NavHost
composable<Profile> { backStackEntry ->
    val profile = backStackEntry.toRoute<Profile>()
    ProfileScreen(userId = profile.id) { friendUserId ->
        navController.navigate(route = Profile(id = friendUserId))
    }
}

This way, ProfileScreen can be tested independently of Navigation by passing mock values and callbacks.

It is recommended to write tests that cover your app navigation requirements by testing the NavHost, navigation actions passed to your composables, and your individual screen composables.

Test the NavHost

To begin testing your NavHost, add the following navigation-testing dependency to your app module's build.gradle file:

dependencies {
  androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
}

Wrap your app's NavHost in a composable which accepts a NavHostController as a parameter:

@Composable
fun AppNavHost(navController: NavHostController){
  NavHost(navController = navController, startDestination = Home){
      composable<Home> { /*...*/ }
      composable<Profile> { /*...*/ }
  }
}

Now you can test AppNavHost and the navigation logic defined inside NavHost by passing an instance of the navigation testing artifact TestNavHostController.

A UI test that verifies the start destination of your app and NavHost would look like this:

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: TestNavHostController

    @Before
    fun setupAppNavHost() {
        composeTestRule.setContent {
            navController = TestNavHostController(LocalContext.current)
            navController.navigatorProvider.addNavigator(ComposeNavigator())
            AppNavHost(navController = navController)
        }
    }

    @Test
    fun appNavHost_verifyStartDestination() {
        composeTestRule
            .onNodeWithContentDescription("Start Screen")
            .assertIsDisplayed()
    }
}

Test navigation actions

You can test your navigation implementation in multiple ways, by performing clicks on the UI elements and then either verifying the displayed destination or by comparing the expected route against the current route.

As you want to test your concrete app's implementation, clicks on the UI are preferable. To learn how to test this alongside individual composable functions in isolation, see the Testing in Jetpack Compose codelab.

You can also use the navController to check navigation assertions by comparing the current route to the expected one, using the currentBackStackEntry:

@Test
fun appNavHost_clickProfile_navigatesToProfile() {
    composeTestRule.onNodeWithContentDescription("Go to Profile")
        .performClick()

    assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}

For more guidance on Compose testing basics, see the Testing your Compose layout guide.