Handle interruptions and alerts in UI tests
Developer Tools 进阶 20m

在 UI 测试中处理中断和弹窗

Handle interruptions and alerts in UI tests

2020年6月24日

在 Apple 官方观看视频

一句话判断

如果你的 UI 测试经常因为系统弹窗、权限请求或通知打断而失败,这场 Session 给出了系统级的解决方案——而且不需要写丑陋的 sleep 等待。

这场 Session 讲了什么

这场 Session 聚焦于 XCUITest 中一个被广泛忽视但极其重要的话题:如何处理测试执行过程中的各种中断。系统弹窗(如隐私权限请求、证书信任对话框)、通知横幅、来电、低电量提醒——这些不可预测的系统行为是 UI 测试不稳定性的主要来源之一。

演讲者首先介绍了 XCUITest 的 addUIInterruptionMonitor 机制,这是处理系统弹窗的标准方式。然后深入讨论了如何通过 XCUIApplication 的 launchArguments 和 launchEnvironment 预配置权限状态,让测试在已知环境下运行。Session 还覆盖了处理多应用交互测试时(比如从 App 跳转到 Safari 再返回)的中断管理策略。

后半部分,演讲者展示了一些高级技巧,包括如何封装可复用的中断处理器、如何在 Continuous Integration 环境中确保测试稳定性,以及如何调试那些”在我机器上能过”的 flaky test。

值得深挖的点

  • addUIInterruptionMonitor 的生命周期陷阱:中断监视器注册后并非永久有效,它只在你调用 XCUIApplication.run() 之后处于活跃状态。而且如果一个测试中有多个监视器,它们的匹配顺序是后进先出(LIFO)的。理解这个机制对于编写复杂测试流程至关重要,否则你会困惑于”为什么我的弹窗处理器没生效”。
  • 权限预配置 vs 运行时处理:通过 launchArguments 重置权限状态(如 -ResetAuthorizationStatus YES)比依赖 UIInterruptionMonitor 更可靠。但并非所有系统弹窗都能通过这种方式避免,关键在于识别哪些中断可以预防、哪些必须运行时处理。

代码片段

// 注册系统中断监视器处理隐私权限弹窗
let monitor = addUIInterruptionMonitor(
    withDescription: "处理系统权限弹窗"
) { alert in
    if alert.buttons["Allow"].exists {
        alert.buttons["Allow"].tap()
        return true  // 已处理
    }
    return false  // 未处理,交给下一个监视器
}

// 触发需要权限的操作
app.buttons["Request Location"].tap()
// 必须交互一下才能激活监视器
app.tap()
removeUIInterruptionMonitor(monitor)
// 通过 launchArguments 预配置权限,避免弹窗出现
let app = XCUIApplication()
app.launchArguments = [
    "-ResetAuthorizationStatus", "YES",
    "-UITesting", "YES"
]
app.launchEnvironment = [
    "ENABLE_MOCK_LOCATION": "YES"
]
app.launch()

// 现在测试可以在无弹窗环境下运行
// 封装可复用的弹窗处理器
func handleSystemAlerts(
    in app: XCUIApplication,
    timeout: TimeInterval = 5
) {
    let monitor = addUIInterruptionMonitor(
        withDescription: "通用弹窗处理器"
    ) { alert in
        let allowButtons = ["Allow", "OK", "Allow While Using App"]
        for buttonTitle in allowButtons {
            if alert.buttons[buttonTitle].exists {
                alert.buttons[buttonTitle].tap()
                return true
            }
        }
        return false
    }
    // 触发交互以激活监视器
    app.tap()
    removeUIInterruptionMonitor(monitor)
}

最佳实践

  • 优先通过 launchArguments 消除可预测的系统弹窗,而非在运行时逐个处理
  • addUIInterruptionMonitor 注册后必须有一次 UI 交互才能激活,这是很多人踩的坑
  • 在 CI 环境中运行测试时,确保测试设备的通知和来电已关闭
  • 将弹窗处理逻辑封装成独立的 helper 方法,保持测试代码的可读性
  • 避免在测试中使用 sleep() 等待弹窗出现,使用 XCTWaiterexpectation 进行条件等待

还什么值得关注

  • Xcode 12 对 UI 测试的启动速度有优化,测试 bundle 加载更快了
  • 如果你在做多 App 交互测试,注意 XCUIApplication 的 activate/deactivate 方法在不同 iOS 版本上的行为差异
  • 搭配 “Get your test results faster” 一起看,可以建立完整的测试稳定性保障体系
WWDC 2020