请务必在发布应用之前测试其导航逻辑,以便验证应用能否按预期运行。
Navigation 组件可以处理以下所有工作:管理目的地之间的导航、传递参数和使用 FragmentManager
。这些功能已经过严格测试,因此您无需在应用中重新测试它们。但是,务必需要测试的是 Fragment 中的应用专用代码与 Fragment 的 NavController
之间的互动情况。本指南详细介绍了一些常见导航情形及其测试方式。
测试 fragment 导航
为了单独测试 fragment 与其 NavController
之间的交互,Navigation 2.3 及更高版本提供了一个 TestNavHostController
,它提供了用于设置当前目的地的 API,并在 NavController.navigate()
操作后验证返回堆栈。
您可以在应用模块的 build.gradle
文件中添加以下依赖项,以将 Navigation Testing 工件添加到您的项目:
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 开始,当用户点击“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 是否正确地将 NavController
移至 R.id.in_game
屏幕。
您可以结合使用 FragmentScenario
、Espresso 和 TestNavHostController
重新创建测试此场景所需的条件,如以下示例所示:
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
的实例,并将其分配给 Fragment。然后它使用 Espresso 驱动界面,并验证是否执行了相应的导航操作。
和实际 NavController
一样,您必须调用 setGraph
初始化 TestNavHostController
。在本例中,被测 Fragement 便是图表的起始目的地。TestNavHostController
提供了 setCurrentDestination
方法,您可以使用该方法设置当前目的地(以及该目的地的参数,可选),以使 NavController
在测试开始前处于正确的状态。
与 NavHostFragment
会使用的 NavHostController
实例不同,在您调用 navigate()
时,TestNavHostController
不会触发底层 navigate()
行为(例如,FragmentNavigator
会执行的 FragmentTransaction
操作)- 它只更新 TestNavHostController
的状态。
使用 FragmentScenario 测试 NavigationUI
在前面的示例中,提供给 titleScenario.onFragment()
的回调是在 Fragment 在其生命周期中的状态变为 RESUMED
之后调用的。此时,Fragment 的视图已创建和附加完毕,因此这在其生命周期中可能太晚,无法进行正确测试。例如,在 Fragment 中将 NavigationUI
与视图(例如由 Fragment 控制的 Toolbar
)结合使用时,您可以在 Fragment 到达 RESUMED
状态之前使用 NavController
调用设置方法。因此,您需要一种在生命周期的早些时候设置 TestNavHostController
的方法。
拥有自己的 Toolbar
的 Fragment 可以写成如下形式:
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; } });
通过这种方法,NavController
在 onViewCreated()
被调用之前就已可用,因而 Fragment 能够在不引起崩溃的情况下使用 NavigationUI
方法。
测试与返回堆栈条目的交互
当与返回堆栈条目交互时,TestNavHostController
使您可以使用其从 NavHostController
继承的 API 将控制器连接到您自己的测试 LifecycleOwner
、ViewModelStore
和 OnBackPressedDispatcher
。
例如,在测试使用导航分区 ViewModel 的 Fragment 时,必须调用 TestNavHostController
上的 setViewModelStore
:
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())
相关主题
- 构建插桩单元测试 - 了解如何设置插桩测试套件和在 Android 设备上运行测试。
- Espresso - 使用 Espresso 测试应用界面。
- JUnit4 规则与 AndroidX Test - 结合使用 JUnit4 规则与 AndroidX Test 库可提供更高的灵活性,并减少测试中所需的样板代码。
- 测试应用的 Fragment - 了解如何使用
FragmentScenario
单独测试应用 Fragment。 - 针对 AndroidX Test 设置项目 - 了解如何在应用的项目文件中声明使用 AndroidX Test 所需的库。