移动应用和框架的异步特性往往使得编写可靠且可重复的测试变得具有挑战性。当注入用户事件时,测试框架必须等待应用完成对该事件的响应;这种响应可能只是更改屏幕上的某些文本,也可能是活动的完全重建。当测试不具备确定性行为时,它就是不稳定的(flaky)。
Compose 或 Espresso 等现代框架在设计时就考虑了测试,因此可以保证在执行下一个测试操作或断言之前,界面处于空闲状态。这就是同步(synchronization)。
测试同步
当您运行测试无法感知的异步或后台操作(例如从数据库加载数据或显示无限动画)时,仍可能出现问题。
为了提高测试套件的可靠性,您可以安装一种跟踪后台操作的方法,例如 Espresso Idling Resources。此外,您可以替换模块以使用可查询空闲状态的测试版本,或者采用能改善同步的工具,例如用于协程的 TestDispatcher 或用于 RxJava 的 RxIdler。
提高稳定性的方法
大型测试可以同时发现许多回归问题,因为它们测试应用的多个组件。它们通常在模拟器或设备上运行,这意味着它们具有高保真度。虽然大型端到端测试提供了全面的覆盖范围,但它们更容易出现偶发性故障。
减少不稳定性可以采取的主要措施如下:
- 正确配置设备
- 防止同步问题
- 实现重试机制
要使用 Compose 或 Espresso 创建大型测试,通常需要启动其中一个活动并像用户一样进行导航,通过断言或屏幕截图测试来验证界面行为是否正确。
其他框架(例如 UI Automator)允许更大的测试范围,因为您可以与系统界面和其他应用进行交互。但是,UI Automator 测试可能需要更多手动同步,因此它们往往不太可靠。
配置设备
首先,为了提高测试的可靠性,您应该确保设备的操作系统不会意外中断测试的执行。例如,当系统更新对话框显示在其他应用之上,或者磁盘空间不足时。
设备场(Device farm)提供商会配置其设备和模拟器,因此您通常无需采取任何操作。但是,针对特殊情况,它们可能有自己的配置指令。
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 管理的设备包含在设备断开连接时进行重试以及其他改进的机制。
防止同步问题
执行后台或异步操作的组件可能导致测试失败,因为测试语句在界面准备好之前就已执行。随着测试范围的扩大,它变得不稳定的可能性也会增加。这些同步问题是不稳定性的主要来源,因为测试框架需要推断活动是否完成加载,或者是否应该等待更长时间。
解决方案
您可以使用 Espresso 的 Idling Resources 来指示应用何时忙碌,但跟踪每一个异步操作非常困难,尤其是在非常大的端到端测试中。此外,如果在不污染被测代码的情况下安装 Idling Resources 也很困难。
与其估计活动是否忙碌,不如让测试等待直到满足特定条件。例如,您可以等待界面中出现特定的文本或组件。
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 管理的设备。