1. 准备工作
在之前的 Codelab 中,您已学过如何编写和运行单元测试和插桩测试。此 Codelab 将介绍编写测试的一些最佳实践,以及如何添加特定的 Gradle 依赖项以进行测试。此外,您还可以练习编写更多单元测试和插桩测试。
前提条件
- 已在 Android Studio 中打开一个现有项目。
- 已在 Android Studio 中编写过单元测试和插桩测试。
- 具有一定的在 Android Studio 中浏览项目的经验。
- 具有一定的在 Android Studio 中处理
build.gradle
文件的经验。
学习内容
- 编写测试的基础知识。
- 如何添加专用于测试的 Gradle 依赖项?
- 如何使用插桩测试来测试列表?
所需条件
- 一台安装了 Android Studio 的计算机。
- Affirmations 应用的解决方案代码。
下载此 Codelab 的起始代码
在此 Codelab 中,您将在 Affirmations 应用的解决方案代码中添加插桩测试。
- 进入为此项目提供的 GitHub 代码库页面。
- 验证分支名称是否与此 Codelab 中指定的分支名称一致。例如,在以下屏幕截图中,分支名称为 main。
- 在项目的 GitHub 页面上,点击 Code 按钮,以打开一个弹出式窗口。
- 在弹出式窗口中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
- 在计算机上找到该文件(很可能在 Downloads 文件夹中)。
- 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。
在 Android Studio 中打开项目
- 启动 Android Studio。
- 在 Welcome to Android Studio 窗口中,点击 Open。
注意:如果 Android Studio 已经打开,则改为依次选择 File > Open 菜单选项。
- 在文件浏览器中,前往解压缩的项目文件夹所在的位置(很可能在 Downloads 文件夹中)。
- 双击该项目文件夹。
- 等待 Android Studio 打开项目。
- 点击 Run 按钮 以构建并运行应用。请确保该应用按预期构建。
2. 起始应用概览
Affirmations 应用包含一个屏幕,该屏幕向用户显示了一系列与自我肯定话语字词配对的图片。
3. 最佳做法
根据设计,测试代码看起来与应用的业务逻辑不同。这是因为测试本不应包含逻辑;它们只是为了测试逻辑而已。因此,测试不应包含条件语句(如 if
和 when
),也不应包含控制流语句(如 for
和 while
),同样也不应操纵值或进行任何实际计算。
测试有时可能需要使用其中某些代码,但一般情况下您应避免使用。由于我们要在应用中测试此类逻辑,因此,如果我们在测试中包含这类代码,测试可能会失败,具体原因与应用代码运行失败的原因相同。
我们的单元测试应该只从我们的应用中调用测试所必需的代码,并测试因调用这些代码所生成的代码的值或状态。界面测试应只针对界面的预期状态进行测试。您可能一时半会儿还理解不了这个概念,不过没有关系!之后的 Codelab 中会介绍一些主题,帮助您理解此概念。与此同时,我们会编写更多测试,因此请特别注意我们在编写测试时采用的方法。
4. 创建测试目录
在上一个 Codelab 中,您学习了如何为插桩测试创建 androidTest
目录。对于此项目,请对 androidTest
目录和 test
目录重复执行这个过程。这两个目录对应的过程相同,唯一的区别是:对于 test
目录,您必须从 New Directory 下拉菜单中选择 test/java,而非 androidTest/java。为每个名为 com.example.affirmations 的新目录创建一个新软件包。
5. 创建插桩测试类
在 androidTest -> com.example.affirmations 中创建一个名为 AffirmationsListTests.kt
的新类。
与 Dice Roller 应用一样,Affirmations 只有一个 activity。为了测试该 activity 的界面,我们必须明确说明需要启动它。看看您能否回想起自己是如何做到这一点的!
- 将测试运行程序添加到新创建的类中。
@RunWith(AndroidJUnit4::class)
- 为主 activity 创建 activity 场景规则。
@get:Rule
val activity = ActivityScenarioRule(MainActivity::class.java)
- Affirmations 应用会显示一系列图片及其对应的积极自我肯定话语。界面不允许使用者与相关项进行任何互动(例如点击或滑动)。因此,对于此应用,插桩测试仅会测试静态数据。创建一个名为
scroll_to_item()
的测试方法。请注意,它必须带有@Test
注解。
此测试应滚动到列表中包含的特定项。我们尚未介绍过这种办法,因为它需要一个我们的项目尚未引用过的方法。在继续测试之前,我们需要添加一些测试依赖项。
6. 添加插桩测试依赖项
您应该已经熟悉如何添加 Gradle 依赖项以便在应用代码中使用。Gradle 还可让我们专为单元测试和插桩测试添加依赖项。打开位于 app -> build.gradle 的应用级 build.gradle
文件。在依赖项部分中,有三种类型的依赖项实现:implementation
、testImplementation
和 androidTestImplementation
。
implementation
适用于将在应用本身中使用的依赖项,testImplementation
适用于单元测试中使用的依赖项,androidTestImplementation
适用于插桩测试中使用的依赖项。
- 添加一个依赖项以允许在插桩测试中与
RecyclerView
互动。将以下库添加为androidTestImplementation
:
androidx.test.espresso:espresso-contrib:3.4.0
依赖项如下所示:
dependencies {
...
androidTestImplementation
'androidx.test.espresso:espresso-contrib:3.4.0'
}
- 现在同步项目。
7. 测试 RecyclerView
- 项目同步完成后,返回
AffirmationsListTests.kt
文件。提供ViewInteraction
以便对onView()
执行操作。onView()
方法需要传入ViewMatcher
。使用withId()
,同时确保传入用于自我肯定话语的RecyclerView
的 ID。现在,对ViewInteraction
调用perform()
。这正是新添加的依赖项在发挥作用!现在可以传入RecyclerViewActions.scrollToPosition<RecyclerView.Viewholder>(9) ViewAction
。
onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions
.scrollToPosition<RecyclerView.ViewHolder>(9)
)
理解此行的语法不是很关键,但值得探索。顾名思义,RecyclerViewActions
是一个可让您的测试对 RecyclerView
执行操作的类。scrollToPosition()
是 RecyclerViewActions
类中的静态方法,作用是滚动到指定位置。此方法会返回一个所谓的泛型。泛型不在此 Codelab 的讨论范围内,但在本例中,您可以将其视为 scrollToPosition()
方法,该方法会返回 RecyclerView
中的任何项(可以是任何内容)。
在我们的应用中,RecyclerView
中的项是 ViewHolder
,因此我们在方法调用后放置一对尖括号,并在其中指定 RecyclerView.ViewHolder
。最后,传入列表的最终位置 (9
)。
- 现在,已经能够滚动到
RecyclerView
的所需位置,请断言以确保界面会显示预期的信息。确保在您滚动到最后一项内容后,系统会显示与最终自我肯定话语相关的文本。以ViewInteraction
开头,但这次传入新的ViewMatcher
(本例中为withText()
)。对于该方法,请传递包含最终自我肯定话语文本的字符串资源。withText()
方法会根据界面组件显示的文本标识该组件。对于该组件,您只需检查它是否显示了所需的文本。这可以通过对ViewInteraction
调用check()
来实现。check()
需要ViewAssertion
,您可以为后者使用matches()
方法。最后,通过传递isDisplayed()
方法断言会显示界面组件。
onView(withText(R.string.affirmation10))
.check(matches(isDisplayed()))
回到关于对要滚动到的位置进行硬编码的备注,我们可以使用 RecyclerViewActions
来克服此问题。如果您不确定列表的长度,可以使用 scrollTo
操作。scrollTo
函数需要 Matcher<View!>!
才能查找特定项。这可能会涉及很多方面,对于此测试,请使用 withText
。将其应用于您刚才编写的测试,具体代码将如下所示:
onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions
.scrollTo<RecyclerView.ViewHolder>(
withText(R.string.affirmation10)
)
)
onView(withText(R.string.affirmation10))
.check(matches(isDisplayed())
)
现在,一切准备就绪,可以运行测试了。您应该会看到设备或模拟器滚动到列表底部,然后测试会通过。如果要确保测试结果准确无误,请将字符串 ID 替换为 R.string.affirmation1
。滚动后,此字符串资源不会显示,测试应该会失败。
RecyclerViewActions
类中提供了许多方法,建议您了解一下可用的方法。
8. 创建本地测试类
在 test -> com.example.affirmations 中创建一个名为 AffirmationsAdapterTests.kt
的新类。
9. 添加本地测试依赖项
- 在此 Codelab 的前面部分,我们讨论了三种不同类型的依赖项实现,并且您添加了一个进行插桩测试所需的依赖项。现在,您要添加一个进行本地测试所需的依赖项。依次转到 app -> build.gradle,然后将以下代码添加为单元测试依赖项:
org.mockito:mockito-core:3.12.4
依赖项应如下所示:
dependencies {
...
testImplementation 'org.mockito:mockito-core:3.12.4'
}
- 现在同步项目。
10. 测试适配器
此特定应用不适用于单元测试,因为没有太多逻辑可供测试。不过,我们可以再获得一些测试各种组件的经验,为日后的测试做好准备。
- 在单元测试类中添加以下行:
private val context = mock(Context::class.java)
mock()
方法来自我们刚在项目中实现的库。模拟是单元测试不可或缺的一部分,但并不在此 Codelab 的讨论范围内。我们将在另一个 Codelab 中详细介绍模拟。在 Android 中,Context
是应用当前状态的上下文,但请注意,单元测试在 JVM 上运行,而不是在实际设备上运行,因此没有 Context
。借助模拟方法,我们可以创建 Context
的“模拟”实例。它没有任何实际功能,但可用于测试需要上下文的方法。
- 创建一个名为
adapter_size()
的函数,并添加注解将其标记为测试。此测试的目的是确保适配器的大小就是传递给适配器的列表的大小。为此,请创建ItemAdapter
的实例,并传入Datasource
类中的loadAffirmations()
方法返回的列表。您也可以创建一个新的列表并进行测试。对于单元测试,最佳实践是创建自己的测试专用数据,因此我们将为此测试创建自定义列表。
val data = listOf(
Affirmation(R.string.affirmation1, R.drawable.image1),
Affirmation(R.string.affirmation2, R.drawable.image2)
)
- 现在,创建
ItemAdapter
的实例,并传入在前面步骤中创建的context
和data
变量。
val adapter = ItemAdapter(context, data)
Recycler 视图适配器提供了一种方法,该方法会返回名为 getItemCount()
的适配器的大小。对于此应用,该方法如下所示:
/**
* Return the size of your dataset (invoked by the layout manager)
*/
override fun getItemCount() = dataset.size
- 这是应该测试的方法。确保此方法返回的值与您在第 2 步中创建的列表大小一致。使用
assertEquals()
方法,并比较列表大小和适配器大小的值。
assertEquals("ItemAdapter is not the correct size", data.size, adapter.itemCount)
您已经很熟悉 assertEquals()
方法,不过有必要详细检查命令行。第一个参数是一个字符串,如果测试失败,该字符串会显示在测试结果中。第二个参数是预期值。第三个参数是实际值。您的测试类应如下所示:
- 现在,运行测试!
11. 解决方案代码
12. 恭喜
在此 Codelab 中,您:
- 学习了如何添加测试专用依赖项。
- 学习了如何使用插桩测试与
RecyclerView
互动。 - 讨论了一些基本的测试最佳做法。