测试导航组件

1. 准备工作

在之前的 Codelab 中,您已了解了如何使用导航组件进行导航。在本 Codelab 中,您将学习如何测试导航组件。请注意,这与不使用导航组件测试导航不同。

前提条件

  • 已在 Android Studio 中创建过测试目录。
  • 已在 Android Studio 中编写过单元测试和插桩测试。
  • 已在 Android 项目中添加过 Gradle 依赖项。

学习内容

  • 如何使用插桩测试来测试 Navigation 组件。
  • 如何在不重复写代码的情况下设置测试。

所需条件

  • 一台安装了 Android Studio 的计算机。
  • Words 应用的解决方案代码。

下载此 Codelab 的起始代码

在此 Codelab 中,您将向 Words 应用的解决方案代码中添加插桩测试。

  1. 进入为此项目提供的 GitHub 代码库页面。
  2. 验证分支名称是否与此 Codelab 中指定的分支名称一致。例如,在以下屏幕截图中,分支名称为 main

1e4c0d2c081a8fd2.png

  1. 在项目的 GitHub 页面上,点击 Code 按钮,以打开一个弹出式窗口。

1debcf330fd04c7b.png

  1. 在弹出式窗口中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
  2. 在计算机上找到该文件(很可能在 Downloads 文件夹中)。
  3. 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。

在 Android Studio 中打开项目

  1. 启动 Android Studio。
  2. Welcome to Android Studio 窗口中,点击 Open

d8e9dbdeafe9038a.png

注意:如果 Android Studio 已经打开,则改为依次选择 File > Open 菜单选项。

8d1fda7396afe8e5.png

  1. 在文件浏览器中,前往解压缩的项目文件夹所在的位置(很可能在 Downloads 文件夹中)。
  2. 双击该项目文件夹。
  3. 等待 Android Studio 打开项目。
  4. 点击 Run 按钮 8de56cba7583251f.png 以构建并运行应用。请确保该应用按预期构建。

2. 起始应用概览

Words 应用包含一个主屏幕,该屏幕中显示了一个列表,其中的每个列表项都是字母表中的一个字母。点击某个字母会进入另一个屏幕,其中会显示以该字母开头的单词的列表。

3. 创建测试目录

如有必要,像在之前的 Codelab 中操作的那样,为 Words 应用创建一个插桩测试目录。如果您已经完成该操作,则可以跳至添加必要的依赖项的步骤。

4. 创建插桩测试类

androidTest 文件夹中创建一个名为 NavigationTests.kt 的新类。

b023023a2ccc3813.png

5. 添加必要的依赖项

测试导航组件需要一些特定的 Gradle 依赖项。我们还将添加一个依赖项来让我们能够以非常具体的方式测试 fragment。转到应用模块的 build.gradle 文件,并添加以下依赖项:

androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.4.0'
androidTestImplementation 'androidx.navigation:navigation-testing:2.5.2'

debugImplementation 'androidx.fragment:fragment-testing:1.5.3'

现在同步项目。

6. 编写 Navigation 组件测试

测试 Navigation 组件与测试常规导航不同。测试常规导航时,我们会在设备或模拟器上触发执行导航。在测试导航组件时,我们并不会真正让设备或模拟器进行可见的导航,而是会强制导航控制器在不实际更改设备或模拟器上显示内容的情况下进行导航,然后进行检查以确认导航控制器是否到达了正确的位置。

  1. 创建一个名为 navigate_to_words_nav_component() 的测试函数。
  2. 在测试中使用 Navigation 组件需要进行一些设置。在 navigate_to_words_nav_component() 方法中,创建导航控制器的测试实例。
val navController = TestNavHostController(
   ApplicationProvider.getApplicationContext()
)
  1. 导航组件使用 fragment 来驱动界面。有一个与 ActivityScenarioRule 等效的 fragment,可用于隔离 fragment 来进行测试,因此需要特定于该 fragment 的依赖项。这在测试需要进行大量导航才能到达的 fragment 时非常有用,因为可以直接启动该 fragment,而无需添加任何额外代码来处理导航到它的过程。
val letterListScenario = launchFragmentInContainer<LetterListFragment>(themeResId =
R.style.Theme_Words)

我们在此处指定要启动 LetterListFragment。我们必须传递应用的主题,以便界面组件知道要使用哪个主题,否则测试可能会崩溃。

  1. 最后,我们需要明确声明我们希望导航控制器为启动的 fragment 使用的导航图。
letterListScenario.onFragment { fragment ->

   navController.setGraph(R.navigation.nav_graph)

   Navigation.setViewNavController(fragment.requireView(), navController)
}
  1. 现在触发提示导航的事件。
onView(withId(R.id.recycler_view))
   .perform(RecyclerViewActions
       .actionOnItemAtPosition<RecyclerView.ViewHolder>(2, click()))

使用 launchFragmentInContainer() 方法时,无法进行实际的导航,因为容器不知道我们可能会导航到的其他 fragment 或 activity。它只知道我们指定要在其中启动的 fragment。因此,在设备或模拟器上运行此测试时,您将不会看到任何实际的导航过程。这可能看起来不太直观,但可让我们更直接地对当前目的地做出断言。我们不需要定位到已知在特定屏幕上显示的界面组件,而可以检查以确保当前导航控制器的目的地的 ID 与我们期望的 fragment 的 ID 相同。这种方法比前面提到的方法要可靠得多。

assertEquals(navController.currentDestination?.id, R.id.wordListFragment)

您的测试应如下所示:78b4a72f75134ded.png

7. 解决方案代码

8. 利用注释避免出现重复代码

在 Android 中,插桩测试和单元测试都让我们无需重复写代码,即可为类中的每个测试设置相同的配置。

例如,假设我们有一个包含 10 个按钮的 fragment。点击每个按钮都会转到一个唯一的 fragment。

如果我们遵循上述测试中的模式,则必须为 10 项测试中的每一项重复写类似如下的代码(请注意,此代码只是一个示例,不会在我们在此 Codelab 中使用的应用中进行编译):

val navController = TestNavHostController(
    ApplicationProvider.getApplicationContext()
)

val exampleFragmentScenario = launchFragmentInContainer<ExampleFragment>(themeResId =
R.style.Theme_Example)

exampleFragmentScenario.onFragment { fragment ->

   navController.setGraph(R.navigation.example_nav_graph)

   Navigation.setViewNavController(fragment.requireView(), navController)
}

这需要将大量代码重复写 10 次。不过,在这种情况下,我们可以使用 JUnit 提供的 @Before 注解来节省时间。为此,我们需要为一个方法添加注解,然后向该方法提供设置测试所需的代码。我们可以随意命名该方法,但应取一个相关的名称。我们无需设置同一 fragment 10 次,而只需编写一次设置代码,如下所示:

lateinit var navController: TestNavHostController

lateinit var exampleFragmentScenario: FragmentScenario<ExampleFragment>

@Before
fun setup(){
    navController = TestNavHostController(
        ApplicationProvider.getApplicationContext()
    )

    exampleFragmentScenario =  launchFragmentInContainer(themeResId=R.style.Theme_Example)

    exampleFragmentScenario.onFragment { fragment ->

       navController.setGraph(R.navigation.example_nav_graph)

       Navigation.setViewNavController(fragment.requireView(),  navController)
    }
}

现在即可在我们在此类中编写的每个测试中运行此方法,并且可以从其中的任意一个测试访问必要的变量。

同样,如果有在每次测试后都需要执行的代码,我们可以使用 @After 注解。例如,@After 可用于清理用于测试的资源,或者用于插桩测试,用来将设备恢复到特定状态。

JUnit 还提供 @BeforeClass@AfterClass 注解。这里的区别在于,带有此注解的方法只会执行一次,但执行过的代码仍会应用到每个方法。如果您的设置或拆解方法包含成本高昂的操作,则最好改为使用这些注解。带有 @BeforeClass@AfterClass 注解的方法必须放置在伴生对象中,并带有 @JvmStatic 注解。为了演示这些注解的执行顺序,我们来看以下代码:

5157ab00a9b7fb84.png

请注意,@BeforeClass 针对类运行,@Before 在函数之前运行,@After 在函数之后运行,@AfterClass 针对类运行。您能预测到此运行的输出将会是什么样子吗?

39c04aa2ba7b8348.png

这些函数的执行顺序是 setupClass()setupFunction()test_a()tearDownFunction()setupFunction()test_b()tearDownFunction()setupFunction()test_c()tearDownFunction()tearDownClass()。这是合理的,因为 @Before@After 分别会在每个方法之前和之后运行。@BeforeClass 会在该类中的任何内容运行之前运行一次,@AfterClass 则会在该类中的所有内容运行完毕之后运行一次。

9. 恭喜

在本 Codelab 中,您学习了以下内容:

  • 了解了如何测试导航组件。
  • 了解了如何使用 @Before@BeforeClass@After@AfterClass 注解避免重复写代码。