Highlight
XCTest 在 Xcode 12 中能隐式处理 banner notification,并提供
addUIInterruptionMonitor与XCUIApplication.resetAuthorizationStatus(for:),让 UI 测试能分别处理非确定性中断、预期 alert 和隐私权限初始状态。
核心内容
UI 测试有一种很隐蔽的失败路径:测试准备点击一个元素时,另一个 UI 临时挡住了它。recipe 列表里有一个通知横幅,如果它没有盖住当前要点的 cell,测试可以继续。进入详情页后,同一个横幅盖住返回按钮,测试就需要先处理这个面板,再继续点击返回。
Apple 给这个场景一个清晰边界:UI interruption 是非预期、非确定性出现,并且挡住测试目标元素的 UI。banner、alert、dialog、window 都可能成为 interruption。直接由测试动作触发的 alert 不属于这个分类,比如删除一条 recipe 后出现的确认框,它应该作为测试流程的一部分被查询、等待和断言。
XCTest 提供 addUIInterruptionMonitor 注册 closure。多个 handler 会形成栈,后注册的先执行。handler 处理成功就返回 true,测试继续;处理不了就返回 false,XCTest 会继续找下一个 handler。最底层还有 XCTest 自带的隐式 handler:iOS 会处理带 Cancel 或默认按钮的中断,Xcode 12 起还能处理 banner notification;macOS 会处理常见权限对话框和 Bluetooth Setup Assistant。
隐私权限带来另一类问题。系统会记住用户对照片、麦克风、定位等 protected resource 的选择。第一次运行测试后,设备已经不再是干净状态。Xcode 11.4、iOS 和 tvOS 13.4、macOS 10.15.4 开始,XCUIApplication 可以重置 protected resource 的授权状态,让首次授权流程重新变成可重复测试的路径。session 还提醒开发者:这些 alert 来自系统,查询层级要按系统 UI 写;重置授权状态也可能终止 app 进程。
详细内容
先给非确定性中断注册 handler
(05:04)测试先注册一个 interruption monitor。这个 handler 会在 XCTest 发现 alert 挡住目标元素时被调用。最小骨架只拿到 interrupting element,并用返回值告诉 XCTest 是否已经处理。
addUIInterruptionMonitor(withDescription: "Handle recipe update failures") { element -> Bool in
// TODO: Use this handler to retry the update action
return false
}
关键点:
withDescription给 handler 命名,失败诊断时能知道它处理哪类中断。- closure 的
element是挡住当前测试动作的 UI 元素。 - 返回
false表示当前 handler 没有处理,XCTest 会继续调用栈里的下一个 handler。 - handler 会在测试结束时自动移除,也可以在测试中手动移除。
- XCTest 自带的隐式 handler 位于栈底;自定义 handler 返回
false后,才会轮到它尝试 Cancel、默认按钮或 Xcode 12 的 banner notification。
(05:16)recipe 详情测试本身是普通 UI 测试:启动 app,点进 Pancakes,断言标题、图片、食材、步骤,再点返回。问题出在 server 更新 recipe 失败时,app 会弹出 alert,挡住测试下一步要访问的元素。
func testRecipeDetailsNavigation() throws {
let app = XCUIApplication()
app.launch()
let pancakeRecipe = app.cells.staticTexts["Fluffy Pancakes"].firstMatch
pancakeRecipe.tap()
// In the detail view
let detailTitle = app.navigationBars.staticTexts["Fluffy Pancakes"].firstMatch
XCTAssert(detailTitle.waitForExistence(timeout: 30))
let expectedImage = app.images["Fluffy Pancakes Image"].firstMatch
XCTAssert(expectedImage.exists)
let ingredientsTitle = app.staticTexts["Ingredients Title"].firstMatch
XCTAssert(ingredientsTitle.exists)
let ingredientsContent = app.textViews["Ingredients Content"].firstMatch
XCTAssert(ingredientsContent.exists)
XCTAssert((ingredientsContent.value as! String).count > 0)
let instructionsTitle = app.staticTexts["Instructions Title"].firstMatch
XCTAssert(instructionsTitle.exists)
let instructionsTextView = app.textViews["Instructions Content"].firstMatch
XCTAssert(instructionsTextView.exists)
XCTAssert((instructionsTextView.value as! String).count > 0)
// Make sure we received the latest instructions:
let expectedInstructions = """
1. Mix the flour, sugar, baking powder and salt.
2. Pour the milk, melted butter and egg into a well in the center and mix.
3. Heat a frying pan (don't forget to add a little bit of oil, grandson!), and fry until they're golden.
4. Optionally add maple syrup and/or fruits (they're healthy!).
"""
XCTAssertEqual((instructionsTextView.value as! String), expectedInstructions)
// Go back to the list of recipes
let backButton = app.navigationBars.buttons["Grandma's Recipes"].firstMatch
backButton.tap()
XCTAssert(pancakeRecipe.waitForExistence(timeout: 30))
}
关键点:
waitForExistence(timeout:)用在导航结果上,避免页面尚未出现就读取元素。firstMatch明确使用第一个匹配元素,让 query 不在多个候选之间摇摆。- 测试检查完整 instruction 文本,确保拿到的是最新 recipe 数据。
- 后续点击返回按钮时,如果 alert 或 banner 覆盖按钮,interruption handler 才有机会介入。
让 handler 处理真正想要的按钮
(06:20)XCTest 的隐式 handler 可以点 Cancel 或默认按钮。这里的业务目标是拿到最新 recipe,所以测试更应该点 Retry。handler 需要检查 element 是否为 alert,再查找 Retry 按钮。
addUIInterruptionMonitor(withDescription: "Handle recipe update failures") { element -> Bool in
let retryButton = element.buttons["Retry"].firstMatch
if element.elementType == .alert && retryButton.exists {
retryButton.tap()
return true
} else {
return false
}
}
关键点:
element.elementType == .alert把 handler 限定在 alert 场景。element.buttons["Retry"].firstMatch从 interrupting element 内部查找按钮,范围比全局 app query 更窄。retryButton.tap()执行业务上正确的恢复动作。- 返回
true后,XCTest 会继续原来的测试动作。
预期 alert 要进入测试断言
(07:37)删除 recipe 后出现的确认框是预期 alert。它由测试动作直接触发,出现时间可预测。Apple 建议用普通 query 和 wait API 处理它,让 alert 文案和确认动作参与断言。
func testDeleteRecipe() throws {
let breadCell = cell(recipeName: "Banana Bread")
deleteCell(breadCell)
let alert = app.alerts["Delete Recipe"].firstMatch
let alertExists = alert.waitForExistence(timeout: 30)
XCTAssert(alertExists, "Expected alert to show up")
let description = """
Are you sure you want to \
delete this recipe?
"""
let alertDescription = alert.staticTexts[description]
XCTAssert(alertDescription.exists)
alert.buttons["Delete"].tap()
XCTAssertFalse(breadCell.exists)
}
关键点:
deleteCell(breadCell)是触发 alert 的测试动作,alert 出现路径可控。app.alerts["Delete Recipe"].firstMatch直接查询目标 alert。waitForExistence(timeout:)把等待和失败信息留在测试里。- 先断言说明文案,再点 Delete,最后确认原 cell 消失。
重置 protected resource 的授权状态
(10:29)系统会保存照片、麦克风、摄像头、定位等 protected resource 的授权选择。要测试首次请求权限的流程,需要先把 app 对某个资源的授权状态重置,再启动 app。
func testAddingPhotosFirstTime() throws {
let app = XCUIApplication()
app.resetAuthorizationStatus(for: .photos)
app.launch()
// Test code…
}
关键点:
resetAuthorizationStatus(for: .photos)让 app 像从未请求过照片权限一样运行。- reset photos 授权状态可能终止 app 进程,所以示例在 reset 后调用
launch()。 - 权限 alert 来自系统,query 需要按系统 alert 的层级写。
- 这个 API 适合把允许、拒绝、首次启动引导流程拆成可重复的 UI 测试。
- session 明确提到的跨平台资源包括 Contacts、Calendar、Photos、microphone、camera 和 location;iOS 还覆盖 Keyboard、Network Access、Bluetooth,以及 Xcode 12 与 iOS 14 的 Health。
核心启发
-
做什么:给网络更新失败的 UI 测试加 Retry handler。 为什么值得做:10220 的 demo 里,server 偶发失败会弹出 alert,默认 Cancel 会让测试继续使用旧数据。 怎么开始:在测试 setup 中注册
addUIInterruptionMonitor,只在elementType == .alert且存在Retry按钮时点击它并返回true。 -
做什么:把确认删除、放弃编辑这类预期 alert 写成显式断言。 为什么值得做:预期 alert 是用户流程的一部分,测试应该检查标题、说明文本和确认按钮。 怎么开始:用
app.alerts["Alert Title"].firstMatch查询,配合waitForExistence(timeout:)等待,再断言staticTexts和最终 UI 状态。 -
做什么:为照片、麦克风、定位权限写首次授权测试矩阵。 为什么值得做:系统会保存授权决定,不重置状态就很难重复覆盖首次请求路径。 怎么开始:在每个权限测试开头调用
app.resetAuthorizationStatus(for:),随后重新launch(),系统 alert 用系统 UI 层级查询,再测试允许和拒绝分支。 -
做什么:把 interruption handler 分成通用层和业务层。 为什么值得做:XCTest 会按后进先出的顺序调用多个 handler,业务测试可以先处理特定 alert,兜底 handler 再处理 cancel/default 类中断。 怎么开始:为每类非确定性 alert 写带 description 的 handler,只有处理成功才返回
true,无法识别的 element 统一返回false。
关联 Session
- XCTSkip your tests — 用 XCTSkip 让不适用的测试在运行时跳过,减少测试结果噪音。
- Get your test results faster — 继续讲 Xcode 测试工作流、超时、并行化和更快反馈。
- Triage test failures with XCTIssue — 用 XCTIssue 把测试失败记录成更容易定位的问题。
- Write tests to fail — 从测试设计入手,让 UI 测试失败时指向真实原因。
评论
GitHub Issues · utterances