axiom-ui-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUI Testing
UI测试
Overview
概述
Wait for conditions, not arbitrary timeouts. Core principle Flaky tests come from guessing how long operations take. Condition-based waiting eliminates race conditions.
NEW in WWDC 2025: Recording UI Automation allows you to record interactions, replay across devices/languages, and review video recordings of test runs.
等待条件,而非任意超时。核心原则:不稳定测试源于猜测操作所需时长。基于条件的等待可消除竞争条件。
WWDC 2025 新功能:UI自动化录制功能允许你录制交互操作,在不同设备/语言环境下重放,并查看测试运行的视频录制内容。
Example Prompts
示例提问
These are real questions developers ask that this skill is designed to answer:
以下是开发者实际会提出的、本技能可解答的问题:
1. "My UI tests pass locally on my Mac but fail in CI. How do I make them more reliable?"
1. "我的UI测试在本地Mac上能通过,但在CI环境中失败。如何提高测试的可靠性?"
→ The skill shows condition-based waiting patterns that work across devices/speeds, eliminating CI timing differences
→ 本技能会展示适用于不同设备/速度的基于条件的等待模式,消除CI环境中的时序差异
2. "My tests use sleep(2) and sleep(5) but they're still flaky. How do I replace arbitrary timeouts with real conditions?"
2. "我的测试使用sleep(2)和sleep(5)但仍然不稳定。如何用真实条件替换任意超时?"
→ The skill demonstrates waitForExistence, XCTestExpectation, and polling patterns for data loads, network requests, and animations
→ 本技能会演示waitForExistence、XCTestExpectation以及针对数据加载、网络请求和动画的轮询模式
3. "I just recorded a test using Xcode 26's Recording UI Automation. How do I review the video and debug failures?"
3. "我刚用Xcode 26的UI自动化录制功能录制了一个测试。如何查看视频并调试失败的测试?"
→ The skill covers Video Debugging workflows to analyze recordings and find the exact step where tests fail
→ 本技能涵盖视频调试工作流,可分析录制内容并定位测试失败的具体步骤
4. "My test is failing on iPad but passing on iPhone. How do I write tests that work across all device sizes?"
4. "我的测试在iPad上失败,但在iPhone上能通过。如何编写适用于所有设备尺寸的测试?"
→ The skill explains multi-factor testing strategies and device-independent predicates for robust cross-device testing
→ 本技能会解释多因素测试策略和设备无关谓词,实现健壮的跨设备测试
5. "I want to write tests that are not flaky. What are the critical patterns I need to know?"
5. "我想编写稳定的测试。需要掌握哪些关键模式?"
→ The skill provides condition-based waiting templates, accessibility-first patterns, and the decision tree for reliable test architecture
→ 本技能提供基于条件的等待模板、无障碍优先模式,以及可靠测试架构的决策树
Red Flags — Test Reliability Issues
红色预警 —— 测试可靠性问题
If you see ANY of these, suspect timing issues:
- Tests pass locally, fail in CI (timing differences)
- Tests sometimes pass, sometimes fail (race conditions)
- Tests use or
sleep()(arbitrary delays)Thread.sleep() - Tests fail with "UI element not found" then pass on retry
- Long test runs (waiting for worst-case scenarios)
如果出现以下任何一种情况,怀疑是时序问题:
- 测试在本地通过,在CI环境中失败(时序差异)
- 测试有时通过,有时失败(竞争条件)
- 测试使用或
sleep()(任意延迟)Thread.sleep() - 测试因"未找到UI元素"失败,重试后又通过
- 测试运行时间过长(等待最坏情况)
Quick Decision Tree
快速决策树
Test failing?
├─ Element not found?
│ └─ Use waitForExistence(timeout:) not sleep()
├─ Passes locally, fails CI?
│ └─ Replace sleep() with condition polling
├─ Animation causing issues?
│ └─ Wait for animation completion, don't disable
└─ Network request timing?
└─ Use XCTestExpectation or waitForExistence测试失败?
├─ 未找到元素?
│ └─ 使用waitForExistence(timeout:)而非sleep()
├─ 本地通过,CI失败?
│ └─ 用条件轮询替换sleep()
├─ 动画导致问题?
│ └─ 等待动画完成,不要禁用动画
└─ 网络请求时序问题?
└─ 使用XCTestExpectation或waitForExistenceCore Pattern: Condition-Based Waiting
核心模式:基于条件的等待
❌ WRONG (Arbitrary Timeout):
swift
func testButtonAppears() {
app.buttons["Login"].tap()
sleep(2) // ❌ Guessing it takes 2 seconds
XCTAssertTrue(app.buttons["Dashboard"].exists)
}✅ CORRECT (Wait for Condition):
swift
func testButtonAppears() {
app.buttons["Login"].tap()
let dashboard = app.buttons["Dashboard"]
XCTAssertTrue(dashboard.waitForExistence(timeout: 5))
}❌ 错误示例(任意超时):
swift
func testButtonAppears() {
app.buttons["Login"].tap()
sleep(2) // ❌ 猜测操作需要2秒
XCTAssertTrue(app.buttons["Dashboard"].exists)
}✅ 正确示例(等待条件):
swift
func testButtonAppears() {
app.buttons["Login"].tap()
let dashboard = app.buttons["Dashboard"]
XCTAssertTrue(dashboard.waitForExistence(timeout: 5))
}Common UI Testing Patterns
常见UI测试模式
Pattern 1: Waiting for Elements
模式1:等待元素出现
swift
// Wait for element to appear
func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool {
return element.waitForExistence(timeout: timeout)
}
// Usage
XCTAssertTrue(waitForElement(app.buttons["Submit"]))swift
// 等待元素出现
func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool {
return element.waitForExistence(timeout: timeout)
}
// 使用示例
XCTAssertTrue(waitForElement(app.buttons["Submit"]))Pattern 2: Waiting for Element to Disappear
模式2:等待元素消失
swift
func waitForElementToDisappear(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool {
let predicate = NSPredicate(format: "exists == false")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
return result == .completed
}
// Usage
XCTAssertTrue(waitForElementToDisappear(app.activityIndicators["Loading"]))swift
func waitForElementToDisappear(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool {
let predicate = NSPredicate(format: "exists == false")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
return result == .completed
}
// 使用示例
XCTAssertTrue(waitForElementToDisappear(app.activityIndicators["Loading"]))Pattern 3: Waiting for Specific State
模式3:等待特定状态
swift
func waitForButton(_ button: XCUIElement, toBeEnabled enabled: Bool, timeout: TimeInterval = 5) -> Bool {
let predicate = NSPredicate(format: "isEnabled == %@", NSNumber(value: enabled))
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: button)
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
return result == .completed
}
// Usage
let submitButton = app.buttons["Submit"]
XCTAssertTrue(waitForButton(submitButton, toBeEnabled: true))
submitButton.tap()swift
func waitForButton(_ button: XCUIElement, toBeEnabled enabled: Bool, timeout: TimeInterval = 5) -> Bool {
let predicate = NSPredicate(format: "isEnabled == %@", NSNumber(value: enabled))
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: button)
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
return result == .completed
}
// 使用示例
let submitButton = app.buttons["Submit"]
XCTAssertTrue(waitForButton(submitButton, toBeEnabled: true))
submitButton.tap()Pattern 4: Accessibility Identifiers
模式4:无障碍标识符
Set in app:
swift
Button("Submit") {
// action
}
.accessibilityIdentifier("submitButton")Use in tests:
swift
func testSubmitButton() {
let submitButton = app.buttons["submitButton"] // Uses identifier, not label
XCTAssertTrue(submitButton.waitForExistence(timeout: 5))
submitButton.tap()
}Why: Accessibility identifiers don't change with localization, remain stable across UI updates.
在应用中设置:
swift
Button("Submit") {
// 操作逻辑
}
.accessibilityIdentifier("submitButton")在测试中使用:
swift
func testSubmitButton() {
let submitButton = app.buttons["submitButton"] // 使用标识符而非标签
XCTAssertTrue(submitButton.waitForExistence(timeout: 5))
submitButton.tap()
}原因:无障碍标识符不会随本地化更改,在UI更新时保持稳定。
Pattern 5: Network Request Delays
模式5:网络请求延迟处理
swift
func testDataLoads() {
app.buttons["Refresh"].tap()
// Wait for loading indicator to disappear
let loadingIndicator = app.activityIndicators["Loading"]
XCTAssertTrue(waitForElementToDisappear(loadingIndicator, timeout: 10))
// Now verify data loaded
XCTAssertTrue(app.cells.count > 0)
}swift
func testDataLoads() {
app.buttons["Refresh"].tap()
// 等待加载指示器消失
let loadingIndicator = app.activityIndicators["Loading"]
XCTAssertTrue(waitForElementToDisappear(loadingIndicator, timeout: 10))
// 验证数据已加载
XCTAssertTrue(app.cells.count > 0)
}Pattern 6: Animation Handling
模式6:动画处理
swift
func testAnimatedTransition() {
app.buttons["Next"].tap()
// Wait for destination view to appear
let destinationView = app.otherElements["DestinationView"]
XCTAssertTrue(destinationView.waitForExistence(timeout: 2))
// Optional: Wait a bit more for animation to settle
// Only if absolutely necessary
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.3))
}swift
func testAnimatedTransition() {
app.buttons["Next"].tap()
// 等待目标视图出现
let destinationView = app.otherElements["DestinationView"]
XCTAssertTrue(destinationView.waitForExistence(timeout: 2))
// 可选:等待动画完全结束
// 仅在绝对必要时使用
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.3))
}Testing Checklist
测试检查清单
Before Writing Tests
编写测试前
- Use accessibility identifiers for all interactive elements
- Avoid hardcoded labels (use identifiers instead)
- Plan for network delays and animations
- Choose appropriate timeouts (2s UI, 10s network)
- 为所有交互元素添加无障碍标识符
- 避免硬编码标签(改用标识符)
- 考虑网络延迟和动画因素
- 选择合适的超时时间(UI操作2秒,网络请求10秒)
When Writing Tests
编写测试时
- Use not
waitForExistence()sleep() - Use predicates for complex conditions
- Test both success and failure paths
- Make tests independent (can run in any order)
- 使用而非
waitForExistence()sleep() - 对复杂条件使用谓词
- 测试成功和失败路径
- 确保测试独立(可按任意顺序运行)
After Writing Tests
编写测试后
- Run tests 10 times locally (catch flakiness)
- Run tests on slowest supported device
- Run tests in CI environment
- Check test duration (if >30s per test, optimize)
- 本地运行测试10次(捕捉不稳定性)
- 在最慢的支持设备上运行测试
- 在CI环境中运行测试
- 检查测试时长(如果单测试超过30秒,进行优化)
Xcode UI Testing Tips
Xcode UI测试技巧
Launch Arguments for Testing
测试启动参数
swift
func testExample() {
let app = XCUIApplication()
app.launchArguments = ["UI-Testing"]
app.launch()
}In app code:
swift
if ProcessInfo.processInfo.arguments.contains("UI-Testing") {
// Use mock data, skip onboarding, etc.
}swift
func testExample() {
let app = XCUIApplication()
app.launchArguments = ["UI-Testing"]
app.launch()
}在应用代码中:
swift
if ProcessInfo.processInfo.arguments.contains("UI-Testing") {
// 使用模拟数据、跳过引导页等
}Faster Test Execution
加快测试执行速度
swift
override func setUpWithError() throws {
continueAfterFailure = false // Stop on first failure
}swift
override func setUpWithError() throws {
continueAfterFailure = false // 首次失败后停止测试
}Debugging Failing Tests
调试失败的测试
swift
func testExample() {
// Take screenshot on failure
addUIInterruptionMonitor(withDescription: "Alert") { alert in
alert.buttons["OK"].tap()
return true
}
// Print element hierarchy
print(app.debugDescription)
}swift
func testExample() {
// 失败时截取屏幕截图
addUIInterruptionMonitor(withDescription: "Alert") { alert in
alert.buttons["OK"].tap()
return true
}
// 打印元素层级
print(app.debugDescription)
}Common Mistakes
常见错误
❌ Using sleep() for Everything
❌ 所有场景都使用sleep()
swift
sleep(5) // ❌ Wastes time if operation completes in 1sswift
sleep(5) // ❌ 如果操作1秒完成,会浪费时间❌ Not Handling Animations
❌ 未处理动画
swift
app.buttons["Next"].tap()
XCTAssertTrue(app.buttons["Back"].exists) // ❌ May fail during animationswift
app.buttons["Next"].tap()
XCTAssertTrue(app.buttons["Back"].exists) // ❌ 动画过程中可能失败❌ Hardcoded Text Labels
❌ 硬编码文本标签
swift
app.buttons["Submit"].tap() // ❌ Breaks with localizationswift
app.buttons["Submit"].tap() // ❌ 本地化后会失效❌ Tests Depend on Each Other
❌ 测试之间存在依赖
swift
// ❌ Test 2 assumes Test 1 ran first
func test1_Login() { /* ... */ }
func test2_ViewDashboard() { /* assumes logged in */ }swift
// ❌ 测试2假设测试1已运行
func test1_Login() { /* ... */ }
func test2_ViewDashboard() { /* 假设已登录 */ }❌ No Timeout Strategy
❌ 没有合理的超时策略
swift
element.waitForExistence(timeout: 100) // ❌ Too long
element.waitForExistence(timeout: 0.1) // ❌ Too shortUse appropriate timeouts:
- UI animations: 2-3 seconds
- Network requests: 10 seconds
- Complex operations: 30 seconds max
swift
element.waitForExistence(timeout: 100) // ❌ 超时时间过长
element.waitForExistence(timeout: 0.1) // ❌ 超时时间过短使用合适的超时时间:
- UI动画:2-3秒
- 网络请求:10秒
- 复杂操作:最多30秒
Real-World Impact
实际效果
Before (using sleep()):
- Test suite: 15 minutes (waiting for worst-case)
- Flaky tests: 20% failure rate
- CI failures: 50% require retry
After (condition-based waiting):
- Test suite: 5 minutes (waits only as needed)
- Flaky tests: <2% failure rate
- CI failures: <5% require retry
Key insight Tests finish faster AND are more reliable when waiting for actual conditions instead of guessing times.
使用sleep()之前:
- 测试套件耗时:15分钟(等待最坏情况)
- 不稳定测试失败率:20%
- CI失败率:50%需要重试
使用基于条件的等待之后:
- 测试套件耗时:5分钟(仅在需要时等待)
- 不稳定测试失败率:<2%
- CI失败率:<5%需要重试
关键见解:当等待实际条件而非猜测时长时,测试完成速度更快且更可靠。
Recording UI Automation
UI自动化录制
Overview
概述
NEW in Xcode 26: Record, replay, and review UI automation tests with video recordings.
Three Phases:
- Record — Capture interactions (taps, swipes, hardware button presses) as Swift code
- Replay — Run across multiple devices, languages, regions, orientations
- Review — Watch video recordings, analyze failures, view UI element overlays
Supported Platforms: iOS, iPadOS, macOS, watchOS, tvOS, axiom-visionOS (Designed for iPad)
Xcode 26 新功能:录制、重放和查看带视频录制的UI自动化测试。
三个阶段:
- 录制 —— 将交互操作(点击、滑动、硬件按键按下)捕获为Swift代码
- 重放 —— 在多设备、多语言、多地区、多方向下运行测试
- 回顾 —— 观看视频录制内容、分析失败原因、查看UI元素叠加层
支持平台:iOS、iPadOS、macOS、watchOS、tvOS、axiom-visionOS(为iPad设计)
How UI Automation Works
UI自动化工作原理
Key Principles:
- UI automation interacts with your app as a person does using gestures and hardware events
- Runs completely independently from your app (app models/data not directly accessible)
- Uses accessibility framework as underlying technology
- Tells OS which gestures to perform, then waits for completion synchronously one at a time
Actions include:
- Launching your app
- Interacting with buttons and navigation
- Setting system state (Dark Mode, axiom-localization, etc.)
- Setting simulated location
核心原则:
- UI自动化像用户一样通过手势和硬件事件与应用交互
- 完全独立于应用运行(无法直接访问应用模型/数据)
- 以无障碍框架为底层技术
- 告知操作系统执行哪些手势,然后同步等待操作完成
支持的操作:
- 启动应用
- 与按钮和导航组件交互
- 设置系统状态(深色模式、axiom本地化等)
- 设置模拟位置
Accessibility is the Foundation
无障碍是基础
Critical Understanding: Accessibility provides information directly to UI automation.
What accessibility sees:
- Element types (button, text, image, etc.)
- Labels (visible text)
- Values (current state for checkboxes, etc.)
- Frames (element positions)
- Identifiers (accessibility identifiers — NOT localized)
Best Practice: Great accessibility experience = great UI automation experience.
关键认知:无障碍框架为UI自动化提供直接信息。
无障碍框架可识别的内容:
- 元素类型(按钮、文本、图片等)
- 标签(可见文本)
- 值(复选框等的当前状态)
- 框架(元素位置)
- 标识符(无障碍标识符 —— 不会本地化)
最佳实践:出色的无障碍体验 = 出色的UI自动化体验。
Preparing Your App for Recording
为录制准备应用
Step 1: Add Accessibility Identifiers
步骤1:添加无障碍标识符
SwiftUI:
swift
Button("Submit") {
// action
}
.accessibilityIdentifier("submitButton")
// Make identifiers specific to instance
List(landmarks) { landmark in
LandmarkRow(landmark)
.accessibilityIdentifier("landmark-\(landmark.id)")
}UIKit:
swift
let button = UIButton()
button.accessibilityIdentifier = "submitButton"
// Use index for table cells
cell.accessibilityIdentifier = "cell-\(indexPath.row)"Good identifiers are:
- ✅ Unique within entire app
- ✅ Descriptive of element contents
- ✅ Static (don't react to content changes)
- ✅ Not localized (same across languages)
Why identifiers matter:
- Titles/descriptions may change, identifiers remain stable
- Work across localized strings
- Uniquely identify elements with dynamic content
Pro Tip: Use Xcode coding assistant to add identifiers:
Prompt: "Add accessibility identifiers to the relevant parts of this view"SwiftUI:
swift
Button("Submit") {
// 操作逻辑
}
.accessibilityIdentifier("submitButton")
// 为实例设置特定标识符
List(landmarks) { landmark in
LandmarkRow(landmark)
.accessibilityIdentifier("landmark-\(landmark.id)")
}UIKit:
swift
let button = UIButton()
button.accessibilityIdentifier = "submitButton"
// 为表格单元格使用索引
cell.accessibilityIdentifier = "cell-\(indexPath.row)"优秀的标识符具备以下特点:
- ✅ 在整个应用中唯一
- ✅ 能描述元素内容
- ✅ 静态(不随内容变化)
- ✅ 不本地化(在所有语言中保持一致)
标识符的重要性:
- 标题/描述可能更改,但标识符保持稳定
- 适用于本地化字符串
- 可唯一标识动态/本地化内容的元素
专业提示:使用Xcode代码助手添加标识符:
提示:"为该视图的相关部分添加无障碍标识符"Step 2: Review Accessibility with Accessibility Inspector
步骤2:用无障碍检查器审核无障碍性
Launch Accessibility Inspector:
- Xcode menu → Open Developer Tool → Accessibility Inspector
- Or: Launch from Spotlight
Features:
- Element Inspector — List accessibility values for any view
- Property details — Click property name for documentation
- Platform support — Works on all Apple platforms
What to check:
- Elements have labels
- Interactive elements have types (button, not just text)
- Values set for stateful elements (checkboxes, toggles)
- Identifiers set for elements with dynamic/localized content
Sample Code Reference: Delivering an exceptional accessibility experience
启动无障碍检查器:
- Xcode菜单 → 打开开发者工具 → 无障碍检查器
- 或:通过Spotlight启动
功能:
- 元素检查器 —— 列出任意视图的无障碍属性值
- 属性详情 —— 点击属性名称查看文档
- 平台支持 —— 适用于所有Apple平台
需要检查的内容:
- 元素有标签
- 交互元素有类型(如按钮,而非仅文本)
- 有状态的元素(复选框、开关)已设置值
- 动态/本地化内容的元素已设置标识符
代码参考:提供出色的无障碍体验
Step 3: Add UI Testing Target
步骤3:添加UI测试目标
- Open project settings in Xcode
- Click "+" below targets list
- Select UI Testing Bundle
- Click Finish
Result: New UI test folder with template tests added to project.
- 在Xcode中打开项目设置
- 点击目标列表下方的"+"
- 选择UI测试包
- 点击完成
结果:项目中新增UI测试文件夹和模板测试。
Recording Interactions
录制交互操作
Starting a Recording (Xcode 26)
开始录制(Xcode 26)
- Open UI test source file
- Popover appears explaining how to start recording (first time only)
- Click "Start Recording" button in editor gutter
- Xcode builds and launches app in Simulator/device
During Recording:
- Interact with app normally (taps, swipes, text entry, etc.)
- Code representing interactions appears in source editor in real-time
- Recording updates as you type (e.g., text field entries)
Stopping Recording:
- Click "Stop Run" button in Xcode
- 打开UI测试源文件
- 弹出提示框说明如何开始录制(仅首次显示)
- 点击编辑器 gutter 中的**"开始录制"**按钮
- Xcode构建并在模拟器/设备中启动应用
录制期间:
- 正常与应用交互(点击、滑动、文本输入等)
- 代表交互的代码会实时显示在源编辑器中
- 录制内容会随你的操作更新(如文本字段输入)
停止录制:
- 点击Xcode中的**"停止运行"**按钮
Example Recording Session
录制会话示例
swift
func testCreateAustralianCollection() {
let app = XCUIApplication()
app.launch()
// Tap "Collections" tab (recorded automatically)
app.tabBars.buttons["Collections"].tap()
// Tap "+" to add new collection
app.navigationBars.buttons["Add"].tap()
// Tap "Edit" button
app.buttons["Edit"].tap()
// Type collection name
app.textFields.firstMatch.tap()
app.textFields.firstMatch.typeText("Max's Australian Adventure")
// Tap "Edit Landmarks"
app.buttons["Edit Landmarks"].tap()
// Add landmarks
app.tables.cells.containing(.staticText, identifier:"Great Barrier Reef").buttons["Add"].tap()
app.tables.cells.containing(.staticText, identifier:"Uluru").buttons["Add"].tap()
// Tap checkmark to save
app.navigationBars.buttons["Done"].tap()
}swift
func testCreateAustralianCollection() {
let app = XCUIApplication()
app.launch()
// 点击"Collections"标签(自动录制)
app.tabBars.buttons["Collections"].tap()
// 点击"+"添加新收藏
app.navigationBars.buttons["Add"].tap()
// 点击"Edit"按钮
app.buttons["Edit"].tap()
// 输入收藏名称
app.textFields.firstMatch.tap()
app.textFields.firstMatch.typeText("Max's Australian Adventure")
// 点击"Edit Landmarks"
app.buttons["Edit Landmarks"].tap()
// 添加地标
app.tables.cells.containing(.staticText, identifier:"Great Barrier Reef").buttons["Add"].tap()
app.tables.cells.containing(.staticText, identifier:"Uluru").buttons["Add"].tap()
// 点击对勾保存
app.navigationBars.buttons["Done"].tap()
}Reviewing Recorded Code
审核录制的代码
After recording, review and adjust queries:
Multiple Options: Each line has dropdown showing alternative ways to address element.
Selection Recommendations:
- For localized strings (text, button labels): Choose accessibility identifier if available
- For deeply nested views: Choose shortest query (stays resilient as app changes)
- For dynamic content (timestamps, temperature): Use generic query or identifier
Example:
swift
// Recorded options for text field:
app.textFields["Collection Name"] // ❌ Breaks if label localizes
app.textFields["collectionNameField"] // ✅ Uses identifier
app.textFields.element(boundBy: 0) // ✅ Position-based
app.textFields.firstMatch // ✅ Generic, shortestChoose shortest, most stable query for your needs.
录制完成后,审核并调整查询方式:
多种选项:每行代码都有下拉菜单,显示定位元素的替代方式。
选择建议:
- 对于本地化字符串(文本、按钮标签):如果有,选择无障碍标识符
- 对于深层嵌套视图:选择最短的查询(应用变更时保持韧性)
- 对于动态内容(时间戳、温度):使用通用查询或标识符
示例:
swift
// 文本字段的录制选项:
app.textFields["Collection Name"] // ❌ 标签本地化后会失效
app.textFields["collectionNameField"] // ✅ 使用标识符
app.textFields.element(boundBy: 0) // ✅ 基于位置
app.textFields.firstMatch // ✅ 通用、最短根据需求选择最短、最稳定的查询方式。
Adding Validations
添加验证逻辑
After recording, add assertions to verify expected behavior:
录制完成后,添加断言验证预期行为:
Wait for Existence
等待元素出现
swift
// Validate collection created
let collection = app.buttons["Max's Australian Adventure"]
XCTAssertTrue(collection.waitForExistence(timeout: 5))swift
// 验证收藏已创建
let collection = app.buttons["Max's Australian Adventure"]
XCTAssertTrue(collection.waitForExistence(timeout: 5))Wait for Property Changes
等待属性变更
swift
// Wait for button to become enabled
let submitButton = app.buttons["Submit"]
XCTAssertTrue(submitButton.wait(for: .enabled, toEqual: true, timeout: 5))swift
// 等待按钮变为可用状态
let submitButton = app.buttons["Submit"]
XCTAssertTrue(submitButton.wait(for: .enabled, toEqual: true, timeout: 5))Combine with XCTAssert
与XCTAssert结合使用
swift
// Fail test if element doesn't appear
let landmark = app.staticTexts["Great Barrier Reef"]
XCTAssertTrue(landmark.waitForExistence(timeout: 5), "Landmark should appear in collection")swift
// 如果元素未出现则测试失败
let landmark = app.staticTexts["Great Barrier Reef"]
XCTAssertTrue(landmark.waitForExistence(timeout: 5), "地标应显示在收藏中")Advanced Automation APIs
高级自动化API
Setup Device State
设置设备状态
swift
override func setUpWithError() throws {
let app = XCUIApplication()
// Set device orientation
XCUIDevice.shared.orientation = .landscapeLeft
// Set appearance mode
app.launchArguments += ["-UIUserInterfaceStyle", "dark"]
// Simulate location
let location = XCUILocation(location: CLLocation(latitude: 37.7749, longitude: -122.4194))
app.launchArguments += ["-SimulatedLocation", location.description]
app.launch()
}swift
override func setUpWithError() throws {
let app = XCUIApplication()
// 设置设备方向
XCUIDevice.shared.orientation = .landscapeLeft
// 设置外观模式
app.launchArguments += ["-UIUserInterfaceStyle", "dark"]
// 设置模拟位置
let location = XCUILocation(location: CLLocation(latitude: 37.7749, longitude: -122.4194))
app.launchArguments += ["-SimulatedLocation", location.description]
app.launch()
}Launch Arguments & Environment
启动参数与环境变量
swift
func testWithMockData() {
let app = XCUIApplication()
// Pass arguments to app
app.launchArguments = ["-UI-Testing", "-UseMockData"]
// Set environment variables
app.launchEnvironment = ["API_URL": "https://mock.api.com"]
app.launch()
}In app code:
swift
if ProcessInfo.processInfo.arguments.contains("-UI-Testing") {
// Use mock data, skip onboarding
}swift
func testWithMockData() {
let app = XCUIApplication()
// 向应用传递参数
app.launchArguments = ["-UI-Testing", "-UseMockData"]
// 设置环境变量
app.launchEnvironment = ["API_URL": "https://mock.api.com"]
app.launch()
}在应用代码中:
swift
if ProcessInfo.processInfo.arguments.contains("-UI-Testing") {
// 使用模拟数据、跳过引导页
}Custom URL Schemes
自定义URL Scheme
swift
// Open app to specific URL
let app = XCUIApplication()
app.open(URL(string: "myapp://landmark/123")!)
// Open URL with system default app (global version)
XCUIApplication.open(URL(string: "https://example.com")!)swift
// 打开应用到特定URL
let app = XCUIApplication()
app.open(URL(string: "myapp://landmark/123")!)
// 使用系统默认应用打开URL(全局版本)
XCUIApplication.open(URL(string: "https://example.com")!)Accessibility Audits in Tests
测试中的无障碍审核
swift
func testAccessibility() throws {
let app = XCUIApplication()
app.launch()
// Perform accessibility audit
try app.performAccessibilityAudit()
}swift
func testAccessibility() throws {
let app = XCUIApplication()
app.launch()
// 执行无障碍审核
try app.performAccessibilityAudit()
}Test Plans for Multiple Configurations
多配置测试计划
Test Plans let you:
- Include/exclude individual tests
- Set system settings (language, region, appearance)
- Configure test properties (timeouts, repetitions, parallelization)
- Associate with schemes for specific build settings
测试计划允许你:
- 包含/排除单个测试
- 设置系统设置(语言、地区、外观)
- 配置测试属性(超时时间、重复次数、并行化)
- 与Scheme关联以使用特定构建设置
Creating Test Plan
创建测试计划
- Create new or use existing test plan
- Add/remove tests on first screen
- Switch to Configurations tab
- 创建新测试计划或使用现有计划
- 在第一个屏幕添加/移除测试
- 切换到配置标签页
Adding Multiple Languages
添加多语言配置
Configurations:
├─ English
├─ German (longer strings)
├─ Arabic (right-to-left)
└─ Hebrew (right-to-left)Each locale = separate configuration in test plan.
Settings:
- Focused for specific locale
- Shared across all configurations
配置:
├─ 英语
├─ 德语(较长字符串)
├─ 阿拉伯语(从右到左)
└─ 希伯来语(从右到左)每个区域设置 = 测试计划中的独立配置。
设置:
- 针对特定区域设置的聚焦配置
- 所有配置共享的通用设置
Video & Screenshot Capture
视频与屏幕截图捕获
In Configurations tab:
- Capture screenshots: On/Off
- Capture video: On/Off
- Keep media: "Only failures" or "On, and keep all"
Defaults: Videos/screenshots kept only for failing runs (for review).
"On, and keep all" use cases:
- Documentation
- Tutorials
- Marketing materials
在配置标签页:
- 捕获屏幕截图:开/关
- 捕获视频:开/关
- 保留媒体:"仅失败测试"或"开启,保留所有"
默认设置:仅为失败的测试保留视频/屏幕截图(用于回顾)。
"开启,保留所有"的使用场景:
- 文档
- 教程
- 营销素材
Replaying Tests in Xcode Cloud
在Xcode Cloud中重放测试
Xcode Cloud = built-in service for:
- Building app
- Running tests
- Uploading to App Store
- All in cloud without using team devices
Workflow configuration:
- Same test plan used locally
- Runs on multiple devices and configurations
- Videos/results available in App Store Connect
Viewing Results:
- Xcode: Xcode Cloud section
- App Store Connect: Xcode Cloud section
- See build info, logs, failure descriptions, video recordings
Team Access: Entire team can see run history and download results/videos.
Xcode Cloud = 内置服务,用于:
- 构建应用
- 运行测试
- 上传到App Store
- 全部在云端完成,无需使用团队设备
工作流配置:
- 使用与本地相同的测试计划
- 在多设备和配置上运行
- 视频/结果可在App Store Connect中查看
查看结果:
- Xcode:Xcode Cloud部分
- App Store Connect:Xcode Cloud部分
- 查看构建信息、日志、失败描述、视频录制内容
团队访问:整个团队都可查看运行历史并下载结果/视频。
Reviewing Test Results with Videos
用视频回顾测试结果
Accessing Test Report
访问测试报告
- Click Test button in Xcode
- Double-click failing run to see video + description
Features:
- Runs dropdown — Switch between video recordings of different configurations (languages, devices)
- Save video — Secondary click → Save
- Play/pause — Video playback with UI interaction overlays
- Timeline dots — UI interactions shown as dots on timeline
- Jump to failure — Click failure diamond on timeline
- 点击Xcode中的测试按钮
- 双击失败的运行记录查看视频+描述
功能:
- 运行记录下拉菜单 —— 在不同配置(语言、设备)的视频录制内容之间切换
- 保存视频 —— 右键 → 保存
- 播放/暂停 —— 视频播放时显示UI交互叠加层
- 时间轴点 —— UI交互在时间轴上显示为点
- 跳转到失败点 —— 点击时间轴上的失败标记
UI Element Overlay at Failure
失败时的UI元素叠加层
At moment of failure:
- Click timeline failure point
- Overlay shows all UI elements present on screen
- Click any element to see code recommendations for addressing it
- Show All — See alternative examples
Workflow:
- Identify what was actually present (vs what test expected)
- Click element to get query code
- Secondary click → Copy code
- View Source → Go directly to test
- Paste corrected code
Example:
swift
// Test expected:
let button = app.buttons["Max's Australian Adventure"]
// But overlay shows it's actually text, not button:
let text = app.staticTexts["Max's Australian Adventure"] // ✅ Correct在失败时刻:
- 点击时间轴上的失败点
- 叠加层显示屏幕上所有UI元素
- 点击任意元素可查看定位该元素的代码建议
- 显示全部 —— 查看替代示例
工作流:
- 识别实际显示的内容(与测试预期对比)
- 点击元素获取查询代码
- 右键 → 复制代码
- 查看源代码 → 直接跳转到测试代码
- 粘贴修正后的代码
示例:
swift
// 测试预期:
let button = app.buttons["Max's Australian Adventure"]
// 但叠加层显示它实际是文本,而非按钮:
let text = app.staticTexts["Max's Australian Adventure"] // ✅ 正确写法Running Test in Different Language
在不同语言环境下运行测试
Click test diamond → Select configuration (e.g., Arabic) → Watch automation run in right-to-left layout.
Validates: Same automation works across languages/layouts.
Recording UI Automation Checklist
UI自动化录制检查清单
Before Recording
录制前
- Add accessibility identifiers to interactive elements
- Review app with Accessibility Inspector
- Add UI Testing Bundle target to project
- Plan workflow to record (user journey)
- 为交互元素添加无障碍标识符
- 用无障碍检查器审核应用
- 为项目添加UI测试包目标
- 规划要录制的工作流(用户旅程)
During Recording
录制中
- Interact naturally with app
- Record complete user journeys (not individual taps)
- Check code generates as you interact
- Stop recording when workflow complete
- 自然地与应用交互
- 录制完整的用户旅程(而非单个点击)
- 检查代码是否随交互生成
- 工作流完成后停止录制
After Recording
录制后
- Review recorded code options (dropdown on each line)
- Choose stable queries (identifiers > labels)
- Add validations (waitForExistence, XCTAssert)
- Add setup code (device state, launch arguments)
- Run test to verify it passes
- 审核录制代码的选项(每行的下拉菜单)
- 选择稳定的查询方式(标识符优先于标签)
- 添加验证逻辑(waitForExistence、XCTAssert)
- 添加设置代码(设备状态、启动参数)
- 运行测试验证是否通过
Test Plan Configuration
测试计划配置
- Create/update test plan
- Add multiple language configurations
- Include right-to-left languages (Arabic, Hebrew)
- Configure video/screenshot capture settings
- Set appropriate timeouts for network tests
- 创建/更新测试计划
- 添加多语言配置
- 包含从右到左语言(阿拉伯语、希伯来语)
- 配置视频/屏幕截图捕获设置
- 为网络测试设置合适的超时时间
Running & Reviewing
运行与回顾
- Run test locally across configurations
- Review video recordings for failures
- Use UI element overlay to debug failures
- Run in Xcode Cloud for team visibility
- Download and share videos if needed
- 在本地多配置下运行测试
- 查看视频录制内容排查失败
- 使用UI元素叠加层调试失败
- 在Xcode Cloud中运行以实现团队可见性
- 如有需要,下载并分享视频
Network Conditioning in Tests
测试中的网络条件模拟
Overview
概述
UI tests can pass on fast networks but fail on 3G/LTE. Network Link Conditioner simulates real-world network conditions to catch timing-sensitive crashes.
Critical scenarios:
- ❌ iPad Pro over Wi-Fi (fast) → pass
- ❌ iPad Pro over 3G (slow) → crash
- ✅ Test both to catch device-specific failures
UI测试在快速网络下能通过,但在3G/LTE网络下可能失败。网络链接调节器模拟真实世界的网络条件,捕获对时序敏感的崩溃。
关键场景:
- ❌ iPad Pro通过Wi-Fi(快速)→ 通过
- ❌ iPad Pro通过3G(慢速)→ 崩溃
- ✅ 同时测试两种场景以捕获设备特定的失败
Setup Network Link Conditioner
设置网络链接调节器
Install Network Link Conditioner:
- Download from Apple's Additional Tools for Xcode
- Search: "Network Link Conditioner"
- Install:
sudo open Network\ Link\ Conditioner.pkg
Verify Installation:
bash
undefined安装网络链接调节器:
- 从Apple的Xcode附加工具下载
- 搜索:"Network Link Conditioner"
- 安装:
sudo open Network\ Link\ Conditioner.pkg
验证安装:
bash
undefinedCheck if installed
检查是否安装
ls ~/Library/Application\ Support/Network\ Link\ Conditioner/
**Enable in Tests**:
```swift
override func setUpWithError() throws {
let app = XCUIApplication()
// Launch with network conditioning argument
app.launchArguments = ["-com.apple.CoreSimulator.CoreSimulatorService", "-networkShaping"]
app.launch()
}ls ~/Library/Application\ Support/Network\ Link\ Conditioner/
**在测试中启用**:
```swift
override func setUpWithError() throws {
let app = XCUIApplication()
// 带网络条件模拟参数启动
app.launchArguments = ["-com.apple.CoreSimulator.CoreSimulatorService", "-networkShaping"]
app.launch()
}Common Network Profiles
常见网络配置文件
3G Profile (most failures occur here):
swift
override func setUpWithError() throws {
let app = XCUIApplication()
// Simulate 3G (type in launch arguments)
app.launchEnvironment = [
"SIMULATOR_UDID": ProcessInfo.processInfo.environment["SIMULATOR_UDID"] ?? "",
"NETWORK_PROFILE": "3G"
]
app.launch()
}Manual Network Conditioning (macOS System Preferences):
- Open System Preferences → Network
- Click "Network Link Conditioner" (installed above)
- Select profile: 3G, LTE, WiFi
- Click "Start"
- Run tests (they'll use throttled network)
3G配置文件(多数失败发生在此场景):
swift
override func setUpWithError() throws {
let app = XCUIApplication()
// 模拟3G(在启动参数中指定)
app.launchEnvironment = [
"SIMULATOR_UDID": ProcessInfo.processInfo.environment["SIMULATOR_UDID"] ?? "",
"NETWORK_PROFILE": "3G"
]
app.launch()
}手动网络条件模拟(macOS系统偏好设置):
- 打开系统偏好设置 → 网络
- 点击"网络链接调节器"(已安装)
- 选择配置文件:3G、LTE、Wi-Fi
- 点击"启动"
- 运行测试(测试会使用限速网络)
Real-World Example: Photo Upload with Network Throttling
实际示例:带网络限速的照片上传
❌ Without Network Conditioning:
swift
func testPhotoUpload() {
app.buttons["Upload Photo"].tap()
// Passes locally (fast network)
XCTAssertTrue(app.staticTexts["Upload complete"].waitForExistence(timeout: 5))
}
// ✅ Passes locally, ❌ FAILS on 3G with timeout✅ With Network Conditioning:
swift
func testPhotoUploadOn3G() {
let app = XCUIApplication()
// Network Link Conditioner running (3G profile)
app.launch()
app.buttons["Upload Photo"].tap()
// Increase timeout for 3G
XCTAssertTrue(app.staticTexts["Upload complete"].waitForExistence(timeout: 30))
// Verify no crash occurred
XCTAssertFalse(app.alerts.element.exists, "App should not crash on 3G")
}Key differences:
- Longer timeout (30s instead of 5s)
- Check for crashes
- Run on slowest expected network
❌ 未使用网络条件模拟:
swift
func testPhotoUpload() {
app.buttons["Upload Photo"].tap()
// 本地通过(快速网络)
XCTAssertTrue(app.staticTexts["Upload complete"].waitForExistence(timeout: 5))
}
// ✅ 本地通过,❌ 在3G网络下因超时失败✅ 使用网络条件模拟:
swift
func testPhotoUploadOn3G() {
let app = XCUIApplication()
// 网络链接调节器已运行(3G配置文件)
app.launch()
app.buttons["Upload Photo"].tap()
// 为3G网络增加超时时间
XCTAssertTrue(app.staticTexts["Upload complete"].waitForExistence(timeout: 30))
// 验证未发生崩溃
XCTAssertFalse(app.alerts.element.exists, "应用在3G网络下不应崩溃")
}关键差异:
- 更长的超时时间(30秒而非5秒)
- 检查是否崩溃
- 在最慢的预期网络下运行
Multi-Factor Testing: Device Size + Network Speed
多因素测试:设备尺寸 + 网络速度
The Problem
问题
Tests can pass on device A but fail on device B due to layout differences + network delays. Multi-factor testing catches these combinations.
Common failure patterns:
- ✅ iPhone 14 Pro (compact, fast network)
- ❌ iPad Pro 12.9 (large, 3G network) → crashes
- ✅ iPhone 15 (compact, LTE)
- ❌ iPhone 12 (older GPU, 3G) → timeout
测试可能在设备A上通过,但因布局差异+网络延迟在设备B上失败。多因素测试可捕获这些组合场景。
常见失败模式:
- ✅ iPhone 14 Pro(紧凑屏幕,快速网络)
- ❌ iPad Pro 12.9(大屏幕,3G网络)→ 崩溃
- ✅ iPhone 15(紧凑屏幕,LTE网络)
- ❌ iPhone 12(旧GPU,3G网络)→ 超时
Test Plan Configuration for Multiple Devices
多设备测试计划配置
Create Test Plan in Xcode:
- File → New → Test Plan
- Select tests to include
- Click "Configurations" tab
- Add configurations for each device/network combo
Example Configuration Matrix:
Configurations:
├─ iPhone 14 Pro + LTE
├─ iPhone 14 Pro + 3G
├─ iPad Pro 12.9 + LTE
├─ iPad Pro 12.9 + 3G (⚠️ Most failures here)
└─ iPhone 12 + 3G (⚠️ Older device)In Test Plan UI:
- Device: iPhone 14 Pro / iPad Pro 12.9
- OS Version: Latest
- Locale: English
- Network Profile: LTE / 3G
在Xcode中创建测试计划:
- 文件 → 新建 → 测试计划
- 选择要包含的测试
- 点击"配置"标签页
- 为每个设备/网络组合添加配置
示例配置矩阵:
配置:
├─ iPhone 14 Pro + LTE
├─ iPhone 14 Pro + 3G
├─ iPad Pro 12.9 + LTE
├─ iPad Pro 12.9 + 3G (⚠️ 多数失败在此场景)
└─ iPhone 12 + 3G (⚠️ 旧设备)在测试计划UI中:
- 设备:iPhone 14 Pro / iPad Pro 12.9
- 系统版本:最新
- 区域设置:英语
- 网络配置文件:LTE / 3G
Programmatic Device-Specific Testing
程序化设备特定测试
swift
import XCTest
final class MultiFactorUITests: XCTestCase {
var deviceModel: String { UIDevice.current.model }
override func setUpWithError() throws {
let app = XCUIApplication()
app.launch()
// Adjust timeouts based on device
switch deviceModel {
case "iPad" where UIScreen.main.bounds.width > 1000:
// iPad Pro - larger layout, slower rendering
app.launchEnvironment["TEST_TIMEOUT"] = "30"
case "iPhone":
// iPhone - compact, standard timeout
app.launchEnvironment["TEST_TIMEOUT"] = "10"
default:
app.launchEnvironment["TEST_TIMEOUT"] = "15"
}
}
func testListLoadingAcrossDevices() {
let app = XCUIApplication()
let timeout = Double(app.launchEnvironment["TEST_TIMEOUT"] ?? "10") ?? 10
app.buttons["Refresh"].tap()
// Wait for list to load (timeout varies by device)
XCTAssertTrue(
app.tables.cells.count > 0,
"List should load on \(deviceModel)"
)
// Verify no crashes
XCTAssertFalse(app.alerts.element.exists)
}
}swift
import XCTest
final class MultiFactorUITests: XCTestCase {
var deviceModel: String { UIDevice.current.model }
override func setUpWithError() throws {
let app = XCUIApplication()
app.launch()
// 根据设备调整超时时间
switch deviceModel {
case "iPad" where UIScreen.main.bounds.width > 1000:
// iPad Pro - 更大的布局,更慢的渲染
app.launchEnvironment["TEST_TIMEOUT"] = "30"
case "iPhone":
// iPhone - 紧凑布局,标准超时
app.launchEnvironment["TEST_TIMEOUT"] = "10"
default:
app.launchEnvironment["TEST_TIMEOUT"] = "15"
}
}
func testListLoadingAcrossDevices() {
let app = XCUIApplication()
let timeout = Double(app.launchEnvironment["TEST_TIMEOUT"] ?? "10") ?? 10
app.buttons["Refresh"].tap()
// 等待列表加载(超时时间因设备而异)
XCTAssertTrue(
app.tables.cells.count > 0,
"列表应在\(deviceModel)上加载"
)
// 验证未崩溃
XCTAssertFalse(app.alerts.element.exists)
}
}Real-World Example: iPad Pro + 3G Crash
实际示例:iPad Pro + 3G崩溃
Scenario: App works on iPhone 14, crashes on iPad Pro over 3G.
Why it crashes:
- iPad Pro has larger layout (landscape)
- 3G network is slow (latency 100ms+)
- Images don't load in time, layout engine crashes
- Single-device testing misses this combo
Test that catches it:
swift
func testLargeLayoutOn3G() {
let app = XCUIApplication()
// Running with Network Link Conditioner on 3G profile
app.launch()
// iPad Pro: Large grid of images
app.buttons["Browse"].tap()
// Wait longer for images on slow network
let firstImage = app.images["photoGrid-0"]
XCTAssertTrue(
firstImage.waitForExistence(timeout: 20),
"First image must load on slow network"
)
// Verify grid loaded without crash
let loadedCount = app.images.matching(identifier: NSPredicate(format: "identifier BEGINSWITH 'photoGrid'")).count
XCTAssertGreater(loadedCount, 5, "Multiple images should load on 3G")
// No alerts (no crashes)
XCTAssertFalse(app.alerts.element.exists, "App should not crash on large device + slow network")
}场景:应用在iPhone 14上正常运行,但在iPad Pro通过3G网络时崩溃。
崩溃原因:
- iPad Pro有更大的布局(横屏)
- 3G网络速度慢(延迟100ms+)
- 图片未能及时加载,布局引擎崩溃
- 单设备测试未覆盖此组合场景
捕获该崩溃的测试:
swift
func testLargeLayoutOn3G() {
let app = XCUIApplication()
// 网络链接调节器已运行3G配置文件
app.launch()
// iPad Pro:大型图片网格
app.buttons["Browse"].tap()
// 为慢速网络等待更长时间加载图片
let firstImage = app.images["photoGrid-0"]
XCTAssertTrue(
firstImage.waitForExistence(timeout: 20),
"第一张图片必须在慢速网络下加载"
)
// 验证网格已加载且未崩溃
let loadedCount = app.images.matching(identifier: NSPredicate(format: "identifier BEGINSWITH 'photoGrid'")).count
XCTAssertGreater(loadedCount, 5, "多张图片应在3G网络下加载")
// 无警告(未崩溃)
XCTAssertFalse(app.alerts.element.exists, "应用在大型设备+慢速网络下不应崩溃")
}Running Multi-Factor Tests in CI
在CI中运行多因素测试
In GitHub Actions or Xcode Cloud:
yaml
- name: Run tests across devices
run: |
xcodebuild -scheme MyApp \
-testPlan MultiDeviceTestPlan \
testTest Plan runs on:
- iPhone 14 Pro + LTE
- iPhone 14 Pro + 3G
- iPad Pro + LTE
- iPad Pro + 3G
Result: Catch device-specific crashes before App Store submission.
在GitHub Actions或Xcode Cloud中:
yaml
- name: Run tests across devices
run: |
xcodebuild -scheme MyApp \
-testPlan MultiDeviceTestPlan \
test测试计划在以下场景运行:
- iPhone 14 Pro + LTE
- iPhone 14 Pro + 3G
- iPad Pro + LTE
- iPad Pro + 3G
结果:在提交到App Store前捕获设备特定的崩溃。
Debugging Crashes Revealed by UI Tests
调试UI测试暴露的崩溃
Overview
概述
UI tests sometimes reveal crashes that don't happen in manual testing. Key insight Automated tests run faster, interact with app differently, and can expose concurrency/timing bugs.
When crashes happen:
- ❌ Manual testing: Can't reproduce (works when you run it)
- ✅ UI Test: Crashes every time (automated repetition finds race condition)
UI测试有时会暴露手动测试无法复现的崩溃。关键见解:自动化测试运行速度更快,与应用的交互方式不同,可暴露并发/时序问题。
崩溃发生时:
- ❌ 手动测试:无法复现(运行时正常)
- ✅ UI测试:每次都崩溃(自动化重复执行发现竞争条件)
Recognizing Test-Revealed Crashes
识别测试暴露的崩溃
Signs in test output:
Failing test: testPhotoUpload
Error: The app crashed while responding to a UI event
App died from an uncaught exception
Stack trace: [EXC_BAD_ACCESS in PhotoViewController]Video shows: App visibly crashes (black screen, immediate termination).
测试输出中的迹象:
失败测试: testPhotoUpload
错误: 应用在响应UI事件时崩溃
应用因未捕获的异常终止
堆栈跟踪: [EXC_BAD_ACCESS in PhotoViewController]视频显示:应用明显崩溃(黑屏、立即终止)。
Systematic Debugging Approach
系统化调试方法
Step 1: Capture Crash Details
步骤1:捕获崩溃详情
Enable detailed logging:
swift
override func setUpWithError() throws {
let app = XCUIApplication()
// Enable all logging
app.launchEnvironment = [
"OS_ACTIVITY_MODE": "debug",
"DYLD_PRINT_STATISTICS": "1"
]
// Enable test diagnostics
if #available(iOS 17, *) {
let options = XCUIApplicationLaunchOptions()
options.captureRawLogs = true
app.launch(options)
} else {
app.launch()
}
}启用详细日志:
swift
override func setUpWithError() throws {
let app = XCUIApplication()
// 启用所有日志
app.launchEnvironment = [
"OS_ACTIVITY_MODE": "debug",
"DYLD_PRINT_STATISTICS": "1"
]
// 启用测试诊断
if #available(iOS 17, *) {
let options = XCUIApplicationLaunchOptions()
options.captureRawLogs = true
app.launch(options)
} else {
app.launch()
}
}Step 2: Reproduce Locally
步骤2:本地复现
swift
func testReproduceCrash() {
let app = XCUIApplication()
app.launch()
// Run exact sequence that causes crash
app.buttons["Browse"].tap()
app.buttons["Photo Album"].tap()
app.buttons["Select All"].tap()
app.buttons["Upload"].tap()
// Should crash here
let uploadButton = app.buttons["Upload"]
XCTAssertFalse(uploadButton.exists, "App crashed (expected)")
// Don't assert - just let it crash and read logs
}Run test with Console logs visible:
- Xcode: View → Navigators → Show Console
- Watch for exception messages
swift
func testReproduceCrash() {
let app = XCUIApplication()
app.launch()
// 运行导致崩溃的精确操作序列
app.buttons["Browse"].tap()
app.buttons["Photo Album"].tap()
app.buttons["Select All"].tap()
app.buttons["Upload"].tap()
// 应在此处崩溃
let uploadButton = app.buttons["Upload"]
XCTAssertFalse(uploadButton.exists, "应用已崩溃(预期)")
// 不要断言 - 只需让它崩溃并读取日志
}在控制台可见的情况下运行测试:
- Xcode:视图 → 导航器 → 显示控制台
- 观察异常消息
Step 3: Analyze Crash Logs
步骤3:分析崩溃日志
Locations:
- Xcode Console (real-time, less detail)
- ~/Library/Logs/DiagnosticMessages/crash_*.log (full details)
- Device Settings → Privacy → Analytics → Analytics Data
Look for:
- Thread that crashed
- Exception type (EXC_BAD_ACCESS, EXC_CRASH, etc.)
- Stack trace showing which method crashed
Example crash log:
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x0000000000000000
Thread 0 Crashed:
0 MyApp 0x0001a234 -[PhotoViewController reloadPhotos:] + 234
1 MyApp 0x0001a123 -[PhotoViewController viewDidLoad] + 180This tells us:
- Crash in
PhotoViewController.reloadPhotos(_:) - Likely null pointer dereference
- Called from
viewDidLoad
位置:
- Xcode控制台(实时,细节较少)
- ~/Library/Logs/DiagnosticMessages/crash_*.log(完整细节)
- 设备设置 → 隐私与安全性 → 分析与改进 → 分析数据
需要查找的内容:
- 崩溃的线程
- 异常类型(EXC_BAD_ACCESS、EXC_CRASH等)
- 显示崩溃方法的堆栈跟踪
示例崩溃日志:
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x0000000000000000
Thread 0 Crashed:
0 MyApp 0x0001a234 -[PhotoViewController reloadPhotos:] + 234
1 MyApp 0x0001a123 -[PhotoViewController viewDidLoad] + 180这表明:
- 崩溃发生在
PhotoViewController.reloadPhotos(_:) - 可能是空指针引用
- 从调用
viewDidLoad
Step 4: Connection to Swift Concurrency Issues
步骤4:与Swift并发问题的关联
Most UI test crashes are concurrency bugs (not specific to UI testing). Reference related skills:
swift
// Common pattern: Race condition in async image loading
class PhotoViewController: UIViewController {
var photos: [Photo] = []
override func viewDidLoad() {
super.viewDidLoad()
// ❌ WRONG: Accessing photos array from multiple threads
Task {
let newPhotos = await fetchPhotos()
self.photos = newPhotos // May crash if main thread access
reloadPhotos() // ❌ Crash here
}
}
}
// ✅ CORRECT: Ensure main thread
class PhotoViewController: UIViewController {
@MainActor
var photos: [Photo] = []
override func viewDidLoad() {
super.viewDidLoad()
Task {
let newPhotos = await fetchPhotos()
await MainActor.run { [weak self] in
self?.photos = newPhotos
self?.reloadPhotos() // ✅ Safe
}
}
}
}For deep crash analysis: See skill for @MainActor patterns and skill for thread-safety issues.
axiom-swift-concurrencyaxiom-memory-debugging大多数UI测试崩溃是并发问题(并非UI测试特有)。参考相关技能:
swift
// 常见模式:异步图片加载中的竞争条件
class PhotoViewController: UIViewController {
var photos: [Photo] = []
override func viewDidLoad() {
super.viewDidLoad()
// ❌ 错误:从多线程访问photos数组
Task {
let newPhotos = await fetchPhotos()
self.photos = newPhotos // 如果主线程访问可能崩溃
reloadPhotos() // ❌ 在此处崩溃
}
}
}
// ✅ 正确:确保在主线程执行
class PhotoViewController: UIViewController {
@MainActor
var photos: [Photo] = []
override func viewDidLoad() {
super.viewDidLoad()
Task {
let newPhotos = await fetchPhotos()
await MainActor.run { [weak self] in
self?.photos = newPhotos
self?.reloadPhotos() // ✅ 安全
}
}
}
}深入崩溃分析:查看技能了解@MainActor模式,查看技能了解线程安全问题。
axiom-swift-concurrencyaxiom-memory-debuggingStep 5: Add Crash-Prevention Tests
步骤5:添加崩溃预防测试
After fixing:
swift
func testPhotosLoadWithoutCrash() {
let app = XCUIApplication()
app.launch()
// Rapid fire interactions that previously caused crash
app.buttons["Browse"].tap()
app.buttons["Photo Album"].tap()
// Load should complete without crash
let photoGrid = app.otherElements["photoGrid"]
XCTAssertTrue(photoGrid.waitForExistence(timeout: 10))
// No alerts (no crash dialogs)
XCTAssertFalse(app.alerts.element.exists)
}修复后:
swift
func testPhotosLoadWithoutCrash() {
let app = XCUIApplication()
app.launch()
// 快速执行之前导致崩溃的交互
app.buttons["Browse"].tap()
app.buttons["Photo Album"].tap()
// 加载应完成且未崩溃
let photoGrid = app.otherElements["photoGrid"]
XCTAssertTrue(photoGrid.waitForExistence(timeout: 10))
// 无警告(无崩溃对话框)
XCTAssertFalse(app.alerts.element.exists)
}Step 6: Stress Test to Verify Fix
步骤6:压力测试验证修复
swift
func testPhotosLoadUnderStress() {
let app = XCUIApplication()
app.launch()
// Repeat the crash-causing action multiple times
for iteration in 0..<10 {
app.buttons["Browse"].tap()
// Wait for load
let grid = app.otherElements["photoGrid"]
XCTAssertTrue(grid.waitForExistence(timeout: 10), "Iteration \(iteration)")
// Go back
app.navigationBars.buttons["Back"].tap()
app.buttons["Refresh"].tap()
}
// Completed without crash
XCTAssertTrue(true, "Stress test passed")
}swift
func testPhotosLoadUnderStress() {
let app = XCUIApplication()
app.launch()
// 重复执行导致崩溃的操作多次
for iteration in 0..<10 {
app.buttons["Browse"].tap()
// 等待加载完成
let grid = app.otherElements["photoGrid"]
XCTAssertTrue(grid.waitForExistence(timeout: 10), "迭代\(iteration)")
// 返回
app.navigationBars.buttons["Back"].tap()
app.buttons["Refresh"].tap()
}
// 已完成且未崩溃
XCTAssertTrue(true, "压力测试通过")
}Prevention Checklist
预防检查清单
Before releasing
发布前
- Run UI tests on slowest network (3G)
- Run on largest device (iPad Pro)
- Run on oldest supported device (iPhone 12)
- Record video of test runs (saves debugging time)
- Check for crashes in logs
- Run stress tests (10x repeated actions)
- Verify @MainActor on UI properties
- Check for race conditions in async code
- 在最慢的网络(3G)上运行UI测试
- 在最大的设备(iPad Pro)上运行测试
- 在最旧的支持设备(iPhone 12)上运行测试
- 录制测试运行视频(节省调试时间)
- 检查日志中的崩溃信息
- 运行压力测试(重复10次操作)
- 验证UI属性使用@MainActor
- 检查异步代码中的竞争条件
Resources
资源
WWDC: 2025-344, 2024-10179, 2023-10175, 2023-10035
Docs: /xctest, /xcuiautomation/recording-ui-automation-for-testing, /xctest/xctwaiter, /accessibility/delivering_an_exceptional_accessibility_experience, /accessibility/performing_accessibility_testing_for_your_app
Note: This skill focuses on reliability patterns and Recording UI Automation. For TDD workflow, see superpowers:test-driven-development.
History: See git log for changes
WWDC: 2025-344, 2024-10179, 2023-10175, 2023-10035
文档: /xctest, /xcuiautomation/recording-ui-automation-for-testing, /xctest/xctwaiter, /accessibility/delivering_an_exceptional_accessibility_experience, /accessibility/performing_accessibility_testing_for_your_app
注意:本技能专注于可靠性模式和UI自动化录制。有关TDD工作流,请查看superpowers:test-driven-development。
历史记录: 查看git log了解变更