在 Android 中使用测试替身

在为元素或系统设计测试策略时,存在三个相关的测试方面

  • 范围:测试涉及多少代码?测试可以验证单个方法、整个应用程序或介于两者之间的任何内容。测试范围是正在测试的内容,通常将其称为被测对象,但也称为被测系统被测单元
  • 速度:测试运行的速度有多快?测试速度可以从毫秒到几分钟不等。
  • 保真度:测试有多“真实”?例如,如果您正在测试的代码的一部分需要发出网络请求,测试代码是否真的发出此网络请求,或者它是否伪造了结果?如果测试确实与网络通信,则表示它具有更高的保真度。权衡是测试可能需要更长的时间才能运行,如果网络出现故障可能会导致错误,或者使用成本高昂。

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

隔离和依赖项

当您测试元素或元素系统时,您会在隔离状态下进行测试。例如,要测试 ViewModel,您无需启动模拟器并启动 UI,因为它不依赖(或不应依赖)Android 框架。

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

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

测试替身的类型

有各种类型的测试替身

伪造(Fake) 一种测试替身,它具有类的“工作”实现,但其实现方式使其适合测试但不适合生产。

示例:内存数据库。

伪造不需要模拟框架,并且轻量级。它们是首选

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

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

存根(Stub) 一种测试替身,其行为按照您的编程方式执行,但对交互没有期望。通常使用模拟框架创建。为了简单起见,伪造比存根更受欢迎。
虚拟(Dummy) 一种传递但未使用的测试替身,例如,如果您只需要将其作为参数提供。

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

间谍(Spy) 真实对象上的包装器,还跟踪一些其他信息,类似于模拟。通常避免使用它们,因为它们会增加复杂性。因此,伪造或模拟比间谍更受欢迎。
影子(Shadow) Robolectric中使用的伪造。

使用伪造的示例

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

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 测试指南。

Robolectric

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

Robolectric 是一个模拟器,因此它不应该取代简单的单元测试,也不应该用于执行兼容性测试。它以在某些情况下降低保真度为代价,提供了速度并降低了成本。对于 UI 测试,一个好的方法是使它们与 Robolectric 和检测到的测试兼容,并根据需要测试功能或兼容性来决定何时运行它们。Espresso 和 Compose 测试都可以在 Robolectric 上运行。