大规模测试稳定性

移动应用和框架的异步性质常常会导致编写可靠且可重复的测试变得非常困难。注入用户事件时,测试框架必须等待应用完成对该事件的响应,这可能包括更改屏幕上的某些文本,也可能包括完全重新创建 activity。如果测试没有确定性行为,则表示测试不稳定

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

测试同步

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

流程图:显示在测试通过之前检查应用是否处于空闲状态的循环
图 1:测试同步。

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

显示同步基于等待固定时间时测试失败的示意图
图 2:在测试中使用 sleep 会导致测试运行缓慢或不可靠。

提高稳定性的方法

由于大测试会测试应用的多个组件,因此可以同时捕获大量回归问题。它们通常在模拟器或设备上运行,这意味着它们具有较高的保真度。虽然大型端到端测试可提供全面的覆盖率,但更容易偶尔失败。

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

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

如需使用 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 管理的设备包含在设备断开连接时重试的机制以及其他改进。

防止同步问题

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

解决方案

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

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

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