在 Android 中使用测试替身

在测试某个元素或元素系统时,您会对其进行隔离测试。例如,要测试 ViewModel,您无需启动模拟器并启动界面,因为它不依赖(或不应该依赖)Android 框架。

但是,被测对象可能依赖其他对象才能工作。例如,ViewModel 可能依赖数据仓库才能工作。

当您需要为被测对象提供依赖项时,一种常见做法是创建测试替身(或测试对象)。测试替身是看起来和行为都与您应用中的组件相似的对象,但它们是在您的测试中创建的,以提供特定的行为或数据。主要优点是它们使您的测试更快、更简单。

测试替身的类型

测试替身有多种类型

假对象 (Fake) 一种测试替身,它具有类的“工作”实现,但其实现方式使其适用于测试,但不适用于生产环境。

示例:内存数据库。

假对象不需要模拟框架,并且是轻量级的。它们是首选

模拟对象 (Mock) 一种测试替身,其行为方式由您编程决定,并且对其交互有期望。如果模拟对象的交互与您定义的要求不匹配,则测试将失败。模拟对象通常使用模拟框架创建以实现所有这些功能。

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

桩对象 (Stub) 一种测试替身,其行为方式由您编程决定,但对其交互没有期望。通常使用模拟框架创建。为求简洁,假对象优于桩对象。
哑对象 (Dummy) 一种测试替身,它被传递但未使用,例如您只是需要将其作为参数提供。

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

间谍对象 (Spy) 对真实对象的包装,也跟踪一些额外信息,类似于模拟对象。通常会避免使用它们,因为会增加复杂性。因此,假对象或模拟对象优于间谍对象。
影子对象 (Shadow) 在 Robolectric 中使用的假对象。

使用假对象示例

假设您想对一个 ViewModel 进行单元测试,该 ViewModel 依赖于名为 UserRepository 的接口,并将第一个用户的名称暴露给 UI。您可以通过实现该接口并返回已知数据来创建假测试替身。

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

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

这个假的 UserRepository 不需要依赖生产版本会使用的本地和远程数据源。该文件位于测试源集中,不会随生产应用一起发布。

A fake dependency can return known data without depending on remote data sources
图 1:单元测试中的假依赖项。

以下测试验证 ViewModel 是否正确地将第一个用户名称暴露给视图。

@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 是由测试人员创建的。但是,在大型测试中替换任意元素可能具有挑战性。

替换组件和依赖注入

当测试无法控制被测系统的创建时,用测试替身替换组件会变得更加复杂,并且要求您的应用架构遵循可测试的设计。

即使是大型端到端测试也可以从使用测试替身中受益,例如一个在您的应用中导航完整用户流程的插桩 UI 测试。在这种情况下,您可能希望使您的测试密封。密封测试避免所有外部依赖项,例如从互联网获取数据。这提高了可靠性和性能。

图 2:一个涵盖应用大部分内容并模拟远程数据的大型测试。

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

后续步骤

测试策略页面展示了如何使用不同类型的测试来提高您的生产力。