在 Android 中使用测试替身

测试元素或元素系统时,您需要单独进行测试。例如,如需测试 ViewModel,您无需启动模拟器并启动界面,因为它不依赖于(或不应依赖于)Android 框架。

不过,被测对象可能依赖其他对象才能正常运行。例如,ViewModel 可能依赖于数据仓库才能正常运行。

当您需要向受测对象提供依赖项时,常见做法是创建测试替身(或测试对象)。测试双是看起来和运行起来像应用中的组件,但是在测试中创建的对象,用于提供特定行为或数据。主要优点是可让您的测试更快、更简单。

test double 的类型

测试双有各种类型:

测试双胞胎具有类的“有效”实现,但其实现方式非常适合测试,但不适合生产环境。

示例:内存中数据库。

虚构对象不需要模拟框架,并且体积较小。它们是首选

模拟 测试双人舞的行为方式由您编程决定,并且对其互动有预期。如果模拟对象的互动不符合您定义的要求,测试将会失败。通常,我们使用模拟框架来创建模拟对象,以实现所有这些操作。

示例:验证数据库中的某个方法是否正好调用一次。

存根 测试替身,会按照您对其行为的编程来行为,但对交互行为不作预期。通常使用模拟框架创建。为简单起见,请优先使用虚构对象,而不是桩。
哑字符 传递但未使用的测试双精度值,例如,您只需将其作为参数提供。

示例:作为点击回调传递的空函数。

Spy 真实对象的封装容器,它还会跟踪一些其他信息,类似于模拟对象。通常,我们会避免使用这些方法,以免增加复杂性。因此,虚构对象或模拟对象优先于桩。
阴影 Robolectric 中使用的模拟对象。

使用虚构的示例

假设您要对依赖于名为 UserRepository 的接口的 ViewModel 进行单元测试,并向界面公开第一位用户的名称。您可以通过实现接口并返回已知数据来创建虚构的测试双重。

object FakeUserRepository : UserRepository {
    fun getUsers() = listOf(UserAlice, UserBob)
}

val const UserAlice = User("Alice")
val const UserBob = User("Bob")

此虚构的 UserRepository 无需依赖于正式版将要使用的本地和远程数据源。该文件位于测试源代码集中,不会随正式版应用一起提供。

虚构依赖项可以返回已知数据,而无需依赖远程数据源
图 1:单元测试中的虚构依赖项。

以下测试用于验证 ViewModel 是否已正确向 View 公开第一个用户姓名。

@Test
fun viewModelA_loadsUsers_showsFirstUser() {
    // Given a VM using fake data
    val viewModel = ViewModelA(FakeUserRepository) // Kicks off data load on init

    // Verify that the exposed data is correct
    assertEquals(viewModel.firstUserName, UserAlice.name)
}

在单元测试中,将 UserRepository 替换为虚构对象非常简单,因为 ViewModel 由测试人员创建。但是,在更大规模的测试中替换任意元素可能比较困难。

替换组件和依赖项注入

如果测试无法控制被测系统的创建,则替换测试双胞胎的组件会变得更加复杂,并且需要应用的架构遵循可测试的设计。

即使是大型端到端测试,也可以从使用测试双胞胎中受益,例如,用于浏览应用中完整用户流程的插桩界面测试。在这种情况下,您可能需要使测试密封。密封式测试可避免所有外部依赖项,例如从互联网提取数据。这有助于提高可靠性和性能。

图 2:涵盖应用大部分内容并伪造远程数据的大型测试。

您可以手动设计应用以实现这种灵活性,但我们建议您使用 Hilt依赖项注入框架在测试时替换应用中的组件。请参阅 Hilt 测试指南

后续步骤

测试策略页面介绍了如何使用不同类型的测试提高工作效率。