大型测试稳定性

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

像 Compose 或 Espresso 这样的现代框架在设计时就考虑到了测试,因此在下一个测试操作或断言之前,UI 会处于空闲状态,这是有一定的保证的。这就是同步

测试同步

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

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

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

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

提高稳定性的方法

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

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

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

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

其他框架(例如UI Automator)允许更大的范围,因为您可以与系统 UI 和其他应用程序进行交互。但是,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 管理的设备包含在设备断开连接和其他改进的情况下重试的机制。

防止同步问题

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

解决方案

您可以使用Espresso 的空闲资源来指示应用程序何时繁忙,但在大型端到端测试中,跟踪每个异步操作都很困难。此外,安装空闲资源也可能很困难,因为它会污染被测试的代码。

与其估计活动是否繁忙,不如让您的测试等待特定条件满足。例如,您可以等待 UI 中显示特定文本或组件。

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

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 管理的设备。