Navigation componentをテストする


Navigation 2.3.0-alpha01からナビゲーションをテストするためのAPIが用意されたので使ってみます。
(2020年2月29日現在、最新バージョンは2.3.0-alpha02です。この記事も2.3.0-alpha02に基づいています。)

従来のNavigationテスト

今までNavigationのテストをする場合、以下のようなテストコードでした。

TitleScreenTest.kt
@RunWith(AndroidJUnit4::class)
class TitleScreenTest {

    @Test
    fun testNavigationToInGameScreen() {
        // Create a mock NavController
        val mockNavController = mock(NavController::class.java)

        // Create a graphical FragmentScenario for the TitleScreen
        val titleScenario = launchFragmentInContainer<TitleScreen>()

        // Set the NavController property on the fragment
        titleScenario.onFragment { fragment ->
            Navigation.setViewNavController(fragment.requireView(), mockNavController)
        }

        // Verify that performing a click prompts the correct Navigation action
        onView(ViewMatchers.withId(R.id.play_btn)).perform(ViewActions.click())
        verify(mockNavController).navigate(R.id.action_title_screen_to_in_game)
    }
}

何をテストしているかというと、R.id.play_btnというViewがクリックされたときに、navigate(R.id.action_title_screen_to_in_gameが呼び出されるかどうか、というのを検証しています。
ただ、呼び出されたからといってActionが望んだ動作をしているのかということまでは保証できません。
例えば、遷移する際にBack stackをpopする場合やargumentを渡す場合などもテストできたら良さそうです。

TestNavHostController

Navigation 2.3.0-alpha01から導入されたのが、TestNavHostControllerです。
https://developer.android.com/reference/kotlin/androidx/navigation/testing/TestNavHostController

前まではNavControllerをモックしていましたが、TestNavHostControllerを使うことでNavController.navigate()後のCurrent destinationやBack stackにアクセスできるようになります。

TitleScreenTest.kt
@RunWith(AndroidJUnit4::class)
class TitleScreenTest {

    @Test
    fun testNavigationToInGameScreen() {
        // Create a TestNavHostController
        val navController = TestNavHostController(
            ApplicationProvider.getApplicationContext())
        navController.setGraph(R.navigation.trivia)

        // Create a graphical FragmentScenario for the TitleScreen
        val titleScenario = launchFragmentInContainer<TitleScreen>()

        // Set the NavController property on the fragment
        titleScenario.onFragment { fragment ->
            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)
    }
}

注目するところは、assertThat(navController.currentDestination?.id).isEqualTo(R.id.in_game)という記述。
viewをクリックした後のCurrent destionationのidが、想定しているdestination (Fragment)のidと一致しているか、という検証をしています。
メソッドが呼び出されたかというテストより信頼できます。

Start destinationではないFragmentでのテストを書きたい場合

TestNavHostController.setCurrentDestination()というメソッドが用意されています。
idをセットすることで指定したdestinationからテストを開始することができます。

StartDestination.kt
val navController = TestNavHostController(
                ApplicationProvider.getApplicationContext()
).apply {
    setGraph(R.navigation.navigation)
    setCurrentDesination(R.id.destination)
}

argumentをテストしたい場合

NavBackStackEntry.argumentsで取得できます。
SafeArgsを利用している場合も同様で、argumentのkeyはnavigation.xmlで指定しているargumentの"name"になります。

Args.kt
val argument = navController.currentBackStackEntry.arguments!!["key"]
assertThat(argument).isEqualTo("hoge")

Back stackをテストしたい場合

TestNavHostController.backStackですべてのback stack (List<NavBackStackEntry>)が取得できます。
もしくは、NavController.previeousBackStackEntryで現在の一つ前にあるstackを取り出すことができます。

Stack.kt
val backStack = navController.backstack
val previous = navController.previousBackStackEntry

assertThat(previous!!.destination.id).isEqualTo(R.id.destination)

参考

https://developer.android.com/guide/navigation/navigation-testing
https://github.com/android/architecture-components-samples/tree/master/NavigationBasicSample