测试导航

请务必在发布应用之前测试其导航逻辑,以便验证应用能否按预期运行。

Navigation 组件可以处理以下所有工作:管理目的地之间的导航、传递参数和使用 FragmentManager。这些功能已经过严格测试,因此您无需在应用中重新测试它们。但是,务必要测试的是 Fragment 中的应用专用代码与 Fragment 的 NavController 之间的互动情况。本指南详细介绍了一些常见导航情形及其测试方式。

测试 Fragment 导航

要单独测试 Fragment 与其 NavController 之间的互动情况,您可以使用 Mockito 在测试中提供模拟 。然后,您可以使用该模拟实现验证与它的互动。

假设您要构建一个知识问答游戏。游戏从 title_screen 开始,当用户点击“PLAY”时会转到 in_game 屏幕。

表示 title_screen 的 Fragment 大致如下所示:

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 屏幕,您的测试需要验证该 Fragment 是否会调用包含操作 R.id.action_title_screen_to_in_gameNavController.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 的模拟实例,并将其分配给该 Fragment。然后它使用 Espresso 驱动界面,并验证是否采取了相应的导航操作。

使用 FragmentScenario 测试 NavigationUI

在前面的示例中,提供给 titleScenario.onFragment() 的回调是在 Fragment 在其生命周期中的状态变为 RESUMED 之后调用的。此时,Fragment 的视图已创建和附加完毕,因此这在其生命周期中可能有点太晚了,无法进行正确测试。例如,在 Fragment 中将 NavigationUI 与视图(例如由 Fragment 控制的 Toolbar)结合使用时,您可以在 Fragment 到达 RESUMED 状态之前使用 NavController 调用设置方法。因此,您需要一种在生命周期的早些时候设置模拟 NavController 的方法。

拥有自己的 Toolbar 的 Fragment 可以写成如下形式:

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

通过这种方法,NavControlleronViewCreated() 被调用之前就已可用,因而 Fragment 能够在不引起崩溃的情况下使用 NavigationUI 方法。