在发布应用之前测试应用的导航逻辑非常重要,以便验证您的应用按预期工作。
导航组件负责管理目标之间的导航、传递参数以及与FragmentManager
协作的所有工作。这些功能已经过严格测试,因此无需在您的应用中再次测试它们。但是,需要测试的是片段中应用特定代码及其NavController
之间的交互。本指南介绍了一些常见的导航场景以及如何测试它们。
测试片段导航
要单独测试片段与其NavController
的交互,Navigation 2.3 及更高版本提供了一个TestNavHostController
,它提供用于设置当前目标并在NavController.navigate()
操作后验证回退堆栈的 API。
您可以通过在应用模块的build.gradle
文件中添加以下依赖项,将导航测试构件添加到您的项目中
Groovy
dependencies { def nav_version = "2.8.4" androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" }
Kotlin
dependencies { val nav_version = "2.8.4" androidTestImplementation("androidx.navigation:navigation-testing:$nav_version") }
假设您正在构建一个琐事游戏。游戏从标题屏幕开始,当用户点击播放时导航到游戏内屏幕。
表示标题屏幕的片段可能如下所示
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); }); } }
为了测试当用户点击播放时应用是否正确地将用户导航到游戏内屏幕,您的测试需要验证此片段是否正确地将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
实例不同,当您调用navigate()
时,TestNavHostController
不会触发底层的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
允许您通过它从NavHostController
继承的API将控制器连接到您自己的测试LifecycleOwner
、ViewModelStore
和OnBackPressedDispatcher
。
例如,在测试使用导航作用域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规则 - 将JUnit 4规则与AndroidX Test库一起使用,以提供更大的灵活性和减少测试中所需的样板代码。
- 测试您的应用的片段 - 了解如何使用
FragmentScenario
单独测试您的应用片段。 - 为AndroidX Test设置项目 - 了解如何在应用的项目文件中声明必要的库以使用AndroidX Test。