在发布之前,测试应用的导航逻辑非常重要,以验证您的应用是否按预期运行。
导航组件负责处理目标之间的导航管理、参数传递以及与 FragmentManager
的协作。这些功能已经过严格测试,因此无需在您的应用中再次测试它们。然而,重要的是测试您的 Fragment 中应用特定代码与其 NavController
的交互。本指南将介绍一些常见的导航场景以及如何测试它们。
测试 Fragment 导航
要单独测试 Fragment 与其 NavController
的交互,Navigation 2.3 及更高版本提供了 TestNavHostController
,它提供了用于设置当前目标并在 NavController.navigate()
操作后验证返回堆栈的 API。
您可以通过在应用模块的 build.gradle
文件中添加以下依赖项,将 Navigation Testing 工件添加到您的项目
Groovy
dependencies { def nav_version = "2.9.0" androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" }
Kotlin
dependencies { val nav_version = "2.9.0" androidTestImplementation("androidx.navigation:navigation-testing:$nav_version") }
假设您正在构建一个问答游戏。游戏从 title_screen 开始,当用户点击“玩”时导航到 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); }); } }
要测试应用在用户点击“玩”时是否正确地将用户导航到 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
。在此示例中,被测试的 Fragment 是我们图表的起始目标。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 测试您的应用界面。
- 适用于 AndroidX Test 的 JUnit4 规则 - 将 JUnit 4 规则与 AndroidX Test 库配合使用,以提供更大的灵活性并减少测试中所需的样板代码。
- 测试您的应用 Fragment - 了解如何使用
FragmentScenario
单独测试您的应用 Fragment。 - 为 AndroidX Test 设置项目 - 了解如何在应用的工程文件中声明所需的库以使用 AndroidX Test。