在 Android 中使用测试替身

为元素或系统设计测试策略时,需要从以下三个方面着手:

  • 范围:测试涉及多少代码?测试可以验证单个方法、整个应用或介于两者之间的某处。被测试范围是“被测范围”,通常称为“被测对象”以及“被测系统”或“被测单元”。
  • 速度:测试的运行速度有多快?测试速度可能从毫秒到几分钟不等。
  • 保真度:测试的“真实”程度如何?例如,如果您要测试的部分代码需要发出网络请求,那么测试代码是真的发出了此网络请求,还是虚假的结果?如果测试实际与网络通信,则意味着它具有较高的保真度。但弊端是,测试可能需要更长时间才能运行完毕,在网络出现故障时可能会导致错误,或者使用成本可能很高。

请参阅测试内容,了解如何开始定义测试策略。

隔离和依赖项

测试某个元素或元素系统时,您可以在“隔离”模式下进行测试。例如,如需测试 ViewModel,您无需启动模拟器和启动界面,因为它不(或不应该)依赖于 Android 框架。

但是,受测对象可能要依赖于其他人才能正常工作。例如,ViewModel 可能依赖于数据存储区才能正常工作。

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

测试替身的类型

测试替身有多种类型:

虚假 一个测试替身,具有类的“有效”实现,但其实现方式使其适合测试,但不适合生产。

示例:内存中数据库。

虚假应用不需要模拟框架,并且是轻量级。这是首选方式。

模拟 一个测试替身,会按照您的编程行为规范,并对其互动有一定的预期。如果模拟的互动不符合您定义的要求,则模拟将无法通过测试。为了实现以上所有目的,通常使用模拟框架创建模拟。

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

一个测试替身,按照您编程的方式进行行为,但对互动没有期望。通常使用模拟框架创建。为简单起见,虚假条目优于桩。
虚拟 一个可以传递但不会被使用的测试替身,例如,如果您只需要将其作为参数提供。

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

间谍 真实对象的封装容器,它还会跟踪一些其他信息,与模拟类似。通常,会因为增加复杂性而避免使用这些方法。因此,伪造或仿冒品要优于间谍。
阴影 Robolectric 中使用的虚假信息。

仿冒产品的示例

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

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

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

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

虚构依赖项无需依赖远程数据源即可返回已知数据
图 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 是由测试人员创建的。不过,在大型测试中替换任意元素可能并非易事。

替换组件和依赖项注入

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

即使是大型的端到端测试,也可以受益于使用测试替身,例如在您的应用中进行完整用户流的插桩界面测试。在这种情况下,您可能需要使测试具有封闭性。封闭测试会避开所有外部依赖项,例如从互联网提取数据。这有助于提高可靠性和性能。

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

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

Robolectric

在 Android 上,您可以使用 Robolectric 框架,该框架提供了一种特殊类型的测试替身。借助 Robolectric,您可以在工作站或持续集成环境中运行测试。它使用常规 JVM,无需模拟器或设备。它使用称为“影子”的测试替身模拟视图膨胀、资源加载和 Android 框架的其他部分。

Robolectric 是一个模拟器,因此它不应取代简单的单元测试,也不应该用于进行兼容性测试。在某些情况下,它可以加快速度并降低费用,但代价是保真度较低。对于界面测试,一种很好的方法是使其与 Robolectric 测试和插桩测试兼容,并根据对功能或兼容性进行测试的需要来决定何时运行这些测试。Espresso 和 Compose 测试都可以在 Robolectric 上运行。