ナビゲーションをテストする

アプリを公開する前にナビゲーション ロジックをテストして、アプリが想定どおりに機能することを検証してください。

Navigation コンポーネントは、デスティネーション間のナビゲーションの管理、引数の受け渡し、FragmentManager の操作に関するすべての作業を処理します。このような機能はすでに厳密にテストされているため、個々のアプリで再度テストする必要はありません。他方、フラグメント内のアプリ固有のコードとその NavController との間のインタラクションに関しては、デベロッパーがテストを行う必要があります。このガイドでは、一般的なナビゲーション シナリオとそのテスト方法について説明します。

フラグメント ナビゲーションをテストする

フラグメントのインタラクションを NavController とは独立にテストするため、Navigation 2.3 以上では、現在のデスティネーションを設定するための API を提供する TestNavHostController が用意されており、NavController.navigate() オペレーションの後でバックスタックを検証できます。

プロジェクトにナビゲーション テストのアーティファクトを追加するには、アプリ モジュールの build.gradle ファイルに次の依存関係を追加します。

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")
}

たとえば、トリビアゲームを作成しているとします。ゲームは title_screen で始まり、ユーザーがプレイをタップすると in_game 画面に移動します。

title_screen を表現するフラグメントは以下のようになります。

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);
        });
    }
}

ユーザーが [Play] をタップしたときに、in_game 画面に適切に移動するかどうかをテストするには、このフラグメントが NavController を正しく R.id.in_game 画面に移動させるかどうかを検証する必要があります。

FragmentScenarioEspressoTestNavHostController を組み合わせて使用すると、このシナリオのテストに必要な条件を再現できます。次の例をご覧ください。

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);
    }
}

上記の例では、TestNavHostController のインスタンスを作成し、それをフラグメントに割り当てています。次に、Espresso を使用して UI を動作させ、適切なナビゲーション アクションが実行されるかどうかを検証しています。

実際の NavController と同じように、setGraph を呼び出して TestNavHostController を初期化する必要があります。この例では、テスト対象のフラグメントはグラフの開始デスティネーションでした。TestNavHostController に用意されている setCurrentDestination メソッドを使用すると、現在のデスティネーション(およびオプションでそのデスティネーションの引数)を設定して、テスト開始前に NavController を正しい状態に置くことができます。

NavHostFragment が使用する NavHostController インスタンスとは異なり、navigate() を呼び出したとき、TestNavHostController はその navigate() の動作(FragmentNavigator が行う FragmentTransaction など)をトリガーしませんTestNavHostController の状態を更新するだけです。

FragmentScenario を使用して NavigationUI をテストする

上記の例では、titleScenario.onFragment() に提供されるコールバックが呼び出されるのは、フラグメントのライフサイクルが RESUMED 状態に移行した後でした。この状態移行の時点で、すでにフラグメントのビューの作成とアタッチは済んでいるため、適切にテストするにはライフサイクル内で遅すぎる可能性があります。たとえば、フラグメントによって制御される Toolbar など、フラグメント内のビューを持つ NavigationUI を使用する場合、フラグメントが RESUMED 状態に到達する前に、NavController を使用してセットアップ メソッドを呼び出すことができます。そのため、ライフサイクル内の早い段階で TestNavHostController を設定する方法が必要となります。

独自の Toolbar を持つフラグメントは、次のように記述できます。

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);
    }
}

この場合、onViewCreated() が呼び出されるまでに、NavController が作成されている必要があります。onFragment() を使用する上記のアプローチでは、ライフサイクル内で TestNavHostController が設定されるタイミングが遅すぎるため、findNavController() 呼び出しが失敗します。

FragmentScenario は、ライフサイクル イベントのコールバックを登録するための FragmentFactory インターフェースを提供します。Fragment.getViewLifecycleOwnerLiveData() と組み合わせることで、onCreateView() の直後に続くコールバックを受信できます。以下をご覧ください。

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;
    }
});

この手法を使用すると、onViewCreated() が呼び出される前に NavController を利用できるようになるため、クラッシュすることなく、フラグメントが NavigationUI メソッドを使用できるようになります。

バックスタック エントリの操作のテスト

TestNavHostController は、バックスタック エントリを操作するとき、NavHostController から継承する API を使って、コントローラを独自のテスト用 LifecycleOwnerViewModelStoreOnBackPressedDispatcher に接続できるようにします。

たとえば、ナビゲーションにスコープ設定された ViewModel を使用するフラグメントをテストするときは、TestNavHostControllersetViewModelStore を呼び出す必要があります。

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())