6 月 3 日の「#Android11: The Beta Launch Show」にぜひご参加ください。

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

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

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

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

NavController を使用してフラグメントのインタラクションを単独でテストするには、Mockito を使用して、テスト内でモック を提供します。このモック実装を使用することで、モックとのインタラクションを検証できます。

たとえば、トリビアゲームを作成しているとします。ゲームは 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 画面に適切に移動するかテストするには、このフラグメントが R.id.action_title_screen_to_in_game アクションを使用して NavController.navigate() を呼び出すかどうかを検証する必要があります。

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

Kotlin

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

Java

    @RunWith(AndroidJUnit4.class)
    public class TitleScreenTestJava {

        @Test
        public void testNavigationToInGameScreen() {

            // Create a mock NavController
            NavController mockNavController = mock(NavController.class);

            // Create a graphical FragmentScenario for the TitleScreen
            FragmentScenario<TitleScreen> titleScenario = FragmentScenario.launchInContainer(TitleScreen.class);

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

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

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

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

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

Kotlin

    class TitleScreen : Fragment() {

        override fun onCreateView(
                inflater: LayoutInflater,
                container: ViewGroup?,
                savedInstanceState: Bundle?
        ) = return inflater.inflate(R.layout.fragment_title_screen, container, false)

        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 {

        @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) {
            NavController navController = Navigation.findNavController(view);
            view.findViewById(R.id.toolbar).setupWithNavController(navController);
        }
    }
    

この場合、onViewCreated() が呼び出されるまでに、NavControllerがモックされている必要があります。上記の onFragment() アプローチを使用した場合、モック NavController が設定されるのがライフサイクル内で遅すぎるため、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 mock NavController
            fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
                if (viewLifecycleOwner != null) {
                    // The fragment’s view has just been created
                    Navigation.setViewNavController(fragment.requireView(), mockNavController)
                }
            }
        }
    }
    

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 mock NavController
            titleScreen.getViewLifecycleOwnerLiveData().observeForever(new Observer<LifecycleOwner>() {
                @Override
                public void onChanged(LifecycleOwner viewLifecycleOwner) {

                    // The fragment’s view has just been created
                    if (viewLifecycleOwner != null) {
                        Navigation.setViewNavController(titleScreen.requireView(), mockNavController);
                    }

                }
            });
            return titleScreen;
        }
    });
    

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