在发布应用之前测试应用的导航逻辑非常重要,以便验证应用是否按预期工作。
导航组件处理管理目标之间导航、传递参数以及与 FragmentManager
协作的所有工作。这些功能已经过严格测试,因此无需在应用中再次测试它们。但是,需要测试的是片段中应用特定代码与其 NavController
之间的交互。本指南介绍了一些常见的导航场景以及如何对其进行测试。
测试片段导航
为了独立测试片段与其 NavController
的交互,Navigation 2.3 及更高版本提供了 TestNavHostController
,它提供了用于设置当前目标并在 NavController.navigate()
操作后验证回退栈的 API。
您可以通过在应用模块的 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
屏幕。
通过结合使用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
的实例,并将其分配给片段。然后,它使用Espresso驱动UI并验证是否采取了适当的导航操作。
就像真实的NavController
一样,您必须调用setGraph
来初始化TestNavHostController
。在此示例中,正在测试的片段是图的起始目标。TestNavHostController
提供了一个setCurrentDestination
方法,允许您设置当前目标(以及可选地,该目标的参数),以便在测试开始之前NavController
处于正确状态。
NavHostFragment
将使用的NavHostController
实例不同,TestNavHostController
在您调用navigate()
时**不会**触发底层的navigate()
行为(例如FragmentNavigator
执行的FragmentTransaction
),它只会更新TestNavHostController
的状态。
使用FragmentScenario测试NavigationUI
在前面的示例中,提供给titleScenario.onFragment()
的回调是在片段的生命周期移动到RESUMED
状态后调用的。此时,片段的视图已经创建并附加,因此在生命周期中可能为时已晚,无法正确测试。例如,当在片段中使用NavigationUI
和视图时,例如使用由片段控制的Toolbar
,您可以在片段到达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; } });
通过使用此技术,NavController
在调用onViewCreated()
之前可用,允许片段使用NavigationUI
方法而不会崩溃。
测试与回退栈条目的交互
当与回退栈条目交互时,TestNavHostController
允许您将控制器连接到您自己的测试LifecycleOwner
、ViewModelStore
和OnBackPressedDispatcher
,方法是使用它从NavHostController
继承的API。
例如,当测试使用导航作用域ViewModel的片段时,您必须在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测试应用的UI。
- 使用AndroidX Test的JUnit4规则 - 使用AndroidX Test库中的JUnit 4规则,以提供更大的灵活性并减少测试中所需的样板代码。
- 测试应用的片段 - 了解如何使用
FragmentScenario
独立测试应用的片段。 - 为AndroidX Test设置项目 - 了解如何在应用的项目文件中声明所需的库以使用AndroidX Test。