大型测试稳定性

移动应用和框架的异步性质通常使得编写可靠且可重复的测试变得具有挑战性。当注入用户事件时,测试框架必须等待应用完成对其的响应,这可能包括从屏幕上更改一些文本到完全重新创建 activity。当测试不具有确定性行为时,它就是不稳定的

Compose 或 Espresso 等现代框架在设计时就考虑到了测试,因此在下一个测试操作或断言之前,可以一定程度上保证界面处于空闲状态。这就是同步

测试同步

当您运行测试不知道的异步或后台操作时,例如从数据库加载数据或显示无限动画时,仍然可能会出现问题。

flow diagram showing a loop that checks if the app is idle before making a test pass
图 1: 测试同步。

为了提高测试套件的可靠性,您可以安装一种跟踪后台操作的方式,例如 Espresso 空闲资源。此外,您还可以替换模块,以使用可以查询空闲状态或提高同步性的测试版本,例如协程的 TestDispatcher 或 RxJava 的 RxIdler

Diagram showing a test failure when the synchronization is based on waiting for a fixed time
图 2: 在测试中使用休眠会导致测试变慢或不稳定。

提高稳定性的方法

大型测试可以同时发现许多回归问题,因为它们测试应用中的多个组件。它们通常在模拟器或设备上运行,这意味着它们具有高保真度。虽然大型端到端测试提供了全面的覆盖,但它们更容易出现偶发性故障。

您可以采取的主要措施来减少不稳定性如下:

  • 正确配置设备
  • 防止同步问题
  • 实现重试

要使用 ComposeEspresso 创建大型测试,您通常会启动您的一个 activity 并像用户一样导航,使用断言或屏幕截图测试来验证界面行为是否正确。

其他框架(例如 UI Automator)允许更大的范围,因为您可以与系统界面和其他应用进行交互。但是,UI Automator 测试可能需要更多手动同步,因此它们往往不太可靠。

配置设备

首先,为了提高测试的可靠性,您应该确保设备的操作系统不会意外中断测试的执行。例如,当系统更新对话框显示在其他应用上方时,或者当磁盘空间不足时。

设备农场提供商会配置其设备和模拟器,因此通常您无需执行任何操作。但是,他们可能对特殊情况有自己的配置指令。

Gradle 管理的设备

如果您自行管理模拟器,可以使用 Gradle 管理的设备来定义用于运行测试的设备

android {
  testOptions {
    managedDevices {
      localDevices {
        create("pixel2api30") {
          // Use device profiles you typically see in Android Studio.
          device = "Pixel 2"
          // Use only API levels 27 and higher.
          apiLevel = 30
          // To include Google services, use "google".
          systemImageSource = "aosp"
        }
      }
    }
  }
}

通过此配置,以下命令将创建模拟器镜像、启动实例、运行测试并将其关闭。

./gradlew pixel2api30DebugAndroidTest

Gradle 管理的设备包含在设备断开连接时重试的机制以及其他改进。

防止同步问题

执行后台或异步操作的组件可能导致测试失败,因为测试语句在界面准备好之前就已执行。随着测试范围的扩大,它变得不稳定的可能性会增加。这些同步问题是不稳定性(flakiness)的主要来源,因为测试框架需要推断 activity 是否已完成加载,或者是否应等待更长时间。

解决方案

您可以使用 Espresso 的空闲资源来指示应用何时忙碌,但很难跟踪每个异步操作,尤其是在非常大型的端到端测试中。此外,在不污染被测代码的情况下安装空闲资源可能很困难。

与其估算 activity 是否忙碌,不如让测试等待直到满足特定条件。例如,您可以等到界面中显示特定文本或组件。

A wait-until mechanism works by asking whether a condition is
  met before continuing.
图 3. 等待条件满足可减少不稳定性。

Compose 拥有一组测试 API,作为 ComposeTestRule 的一部分,用于等待不同的匹配器。

fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)

fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeout: Long = 1000L)

fun waitUntilExactlyOneExists(matcher: SemanticsMatcher,  timeout: Long = 1000L)

fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeout: Long = 1000L)

以及一个接受任何返回布尔值的函数的通用 API。

fun waitUntil(timeoutMillis: Long, condition: () -> Boolean): Unit

示例用法

composeTestRule.waitUntilExactlyOneExists(hasText("Continue")</code>)</p></td>

重试机制

您应该修复不稳定的测试,但有时导致它们失败的条件是如此地不可预测,以至于很难重现。虽然您应该始终跟踪并修复不稳定的测试,但重试机制可以通过多次运行测试直到通过来帮助保持开发人员的生产力。

重试需要在多个层面进行,以防止出现以下问题:

  • 与设备的连接超时或连接丢失
  • 单个测试失败

安装或配置重试取决于您的测试框架和基础设施,但典型机制包括:

  • 一个 JUnit 规则,用于多次重试任何测试
  • CI 工作流程中的重试操作或步骤
  • 当模拟器无响应时,重新启动它的系统,例如 Gradle 管理的设备。