axiom-ui-recording
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRecording UI Automation (Xcode 26+)
UI自动化录制(Xcode 26+)
Guide to Xcode 26's Recording UI Automation feature for creating UI tests through user interaction recording.
本指南介绍Xcode 26的UI自动化录制功能,可通过用户交互录制来创建UI测试。
The Three-Phase Workflow
三阶段工作流
From WWDC 2025-344:
┌─────────────────────────────────────────────────────────────┐
│ UI Automation Workflow │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. RECORD ──────► Interact with app in Simulator │
│ Xcode captures as Swift test code │
│ │
│ 2. REPLAY ──────► Run across devices, languages, configs │
│ Using test plans for multi-config │
│ │
│ 3. REVIEW ──────► Watch video recordings in test report │
│ Analyze failures with screenshots │
│ │
└─────────────────────────────────────────────────────────────┘基于WWDC 2025-344:
┌─────────────────────────────────────────────────────────────┐
│ UI自动化工作流 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 录制 ──────► 在Simulator中与应用交互 │
│ Xcode将操作捕获为Swift测试代码 │
│ │
│ 2. 重放 ──────► 在多设备、多语言、多配置下运行测试 │
│ 使用测试计划实现多配置执行 │
│ │
│ 3. 审查 ──────► 在测试报告中查看视频录制内容 │
│ 通过截图分析测试失败原因 │
│ │
└─────────────────────────────────────────────────────────────┘Phase 1: Recording
阶段1:录制
Starting a Recording
开始录制
- Open your UI test file in Xcode
- Place cursor inside a test method
- Debug → Record UI Automation (or use the record button)
- App launches in Simulator
- Perform interactions - Xcode generates code
- Stop recording when done
- 在Xcode中打开你的UI测试文件
- 将光标置于测试方法内部
- 调试 → 录制UI自动化(或使用录制按钮)
- 应用将在Simulator中启动
- 执行交互操作 - Xcode会生成对应代码
- 完成后停止录制
What Gets Recorded
可录制的操作
- Taps on buttons, cells, controls
- Text input into text fields
- Swipes and scrolling
- Gestures (pinch, rotate)
- Hardware button presses (Home, volume)
- 点击按钮、单元格、控件
- 文本输入到文本字段
- 滑动与滚动
- 手势操作(捏合、旋转)
- 硬件按键按压(Home键、音量键)
Generated Code Example
生成代码示例
swift
// Xcode generates this from your interactions
func testLoginFlow() {
let app = XCUIApplication()
app.launch()
// Recorded: Tap email field, type email
app.textFields["Email"].tap()
app.textFields["Email"].typeText("user@example.com")
// Recorded: Tap password field, type password
app.secureTextFields["Password"].tap()
app.secureTextFields["Password"].typeText("password123")
// Recorded: Tap login button
app.buttons["Login"].tap()
}swift
// Xcode根据你的交互生成此代码
func testLoginFlow() {
let app = XCUIApplication()
app.launch()
// 录制内容:点击邮箱字段,输入邮箱
app.textFields["Email"].tap()
app.textFields["Email"].typeText("user@example.com")
// 录制内容:点击密码字段,输入密码
app.secureTextFields["Password"].tap()
app.secureTextFields["Password"].typeText("password123")
// 录制内容:点击登录按钮
app.buttons["Login"].tap()
}Enhancing Recorded Code
增强录制代码
Critical: Recorded code is often fragile. Always enhance it for stability.
重要提示:录制生成的代码通常稳定性较差,务必对其进行增强以提升稳定性。
1. Add Accessibility Identifiers
1. 添加无障碍标识符
Recorded code uses labels which break with localization:
swift
// RECORDED (fragile - breaks with localization)
app.buttons["Login"].tap()
// ENHANCED (stable - uses identifier)
app.buttons["loginButton"].tap()Add identifiers in your app code:
swift
// SwiftUI
Button("Login") { ... }
.accessibilityIdentifier("loginButton")
// UIKit
loginButton.accessibilityIdentifier = "loginButton"录制代码使用标签,这会在本地化后失效:
swift
// 录制生成的代码(稳定性差 - 本地化后会失效)
app.buttons["Login"].tap()
// 增强后的代码(稳定 - 使用标识符)
app.buttons["loginButton"].tap()在应用代码中添加标识符:
swift
// SwiftUI
Button("Login") { ... }
.accessibilityIdentifier("loginButton")
// UIKit
loginButton.accessibilityIdentifier = "loginButton"2. Add waitForExistence
2. 添加waitForExistence
Recorded code assumes elements exist immediately:
swift
// RECORDED (may fail if app is slow)
app.buttons["Login"].tap()
// ENHANCED (waits for element)
let loginButton = app.buttons["loginButton"]
XCTAssertTrue(loginButton.waitForExistence(timeout: 5))
loginButton.tap()录制代码假设元素会立即存在:
swift
// 录制生成的代码(如果应用响应缓慢可能失败)
app.buttons["Login"].tap()
// 增强后的代码(等待元素加载)
let loginButton = app.buttons["loginButton"]
XCTAssertTrue(loginButton.waitForExistence(timeout: 5))
loginButton.tap()3. Add Assertions
3. 添加断言
Recorded code just performs actions without verification:
swift
// RECORDED (no verification)
app.buttons["Login"].tap()
// ENHANCED (with assertion)
app.buttons["loginButton"].tap()
let welcomeLabel = app.staticTexts["welcomeLabel"]
XCTAssertTrue(welcomeLabel.waitForExistence(timeout: 10),
"Welcome screen should appear after login")录制代码仅执行操作,不进行验证:
swift
// 录制生成的代码(无验证步骤)
app.buttons["Login"].tap()
// 增强后的代码(包含断言)
app.buttons["loginButton"].tap()
let welcomeLabel = app.staticTexts["welcomeLabel"]
XCTAssertTrue(welcomeLabel.waitForExistence(timeout: 10),
"登录后应显示欢迎界面")4. Use Shorter Queries
4. 使用更简洁的查询语句
Recorded code may have overly specific queries:
swift
// RECORDED (too specific)
app.tables.cells.element(boundBy: 0).buttons["Action"].tap()
// ENHANCED (simpler)
app.buttons["actionButton"].tap()录制代码可能包含过于具体的查询:
swift
// 录制生成的代码(过于具体)
app.tables.cells.element(boundBy: 0).buttons["Action"].tap()
// 增强后的代码(更简洁)
app.buttons["actionButton"].tap()Query Selection Guidelines
查询选择指南
From WWDC 2025-344:
| Scenario | Problem | Solution |
|---|---|---|
| Localized strings | "Login" changes by language | Use accessibilityIdentifier |
| Deeply nested views | Long query chains break easily | Use shortest possible query |
| Dynamic content | Cell content changes | Use identifier or generic query |
| Multiple matches | Query returns many elements | Add unique identifier |
来自WWDC 2025-344:
| 场景 | 问题 | 解决方案 |
|---|---|---|
| 本地化字符串 | "Login"会随语言变化 | 使用accessibilityIdentifier |
| 深层嵌套视图 | 长查询链容易失效 | 使用尽可能短的查询语句 |
| 动态内容 | 单元格内容会变化 | 使用标识符或通用查询 |
| 多个匹配结果 | 查询返回多个元素 | 添加唯一标识符 |
Best Practices
最佳实践
- Prefer identifiers over labels
- Use the shortest query that works
- Avoid index-based queries ()
element(boundBy: 0) - Add identifiers to dynamic content
- 优先使用标识符而非标签
- 使用能生效的最短查询语句
- 避免基于索引的查询()
element(boundBy: 0) - 为动态内容添加标识符
Phase 2: Replay with Test Plans
阶段2:使用测试计划重放
Test plans allow running the same tests across multiple configurations.
测试计划允许在多种配置下运行相同的测试。
Creating a Test Plan
创建测试计划
- File → New → File → Test Plan
- Add test targets
- Configure configurations
- 文件 → 新建 → 文件 → 测试计划
- 添加测试目标
- 配置各项参数
Test Plan Structure
测试计划结构
json
{
"configurations": [
{
"name": "iPhone - English",
"options": {
"targetForVariableExpansion": {
"containerPath": "container:MyApp.xcodeproj",
"identifier": "MyApp"
},
"language": "en",
"region": "US"
}
},
{
"name": "iPhone - Spanish",
"options": {
"language": "es",
"region": "ES"
}
},
{
"name": "iPhone - Dark Mode",
"options": {
"userInterfaceStyle": "dark"
}
},
{
"name": "iPad - Landscape",
"options": {
"defaultTestExecutionTimeAllowance": 120,
"testTimeoutsEnabled": true
}
}
],
"defaultOptions": {
"targetForVariableExpansion": {
"containerPath": "container:MyApp.xcodeproj",
"identifier": "MyApp"
}
},
"testTargets": [
{
"target": {
"containerPath": "container:MyApp.xcodeproj",
"identifier": "MyAppUITests",
"name": "MyAppUITests"
}
}
],
"version": 1
}json
{
"configurations": [
{
"name": "iPhone - English",
"options": {
"targetForVariableExpansion": {
"containerPath": "container:MyApp.xcodeproj",
"identifier": "MyApp"
},
"language": "en",
"region": "US"
}
},
{
"name": "iPhone - Spanish",
"options": {
"language": "es",
"region": "ES"
}
},
{
"name": "iPhone - Dark Mode",
"options": {
"userInterfaceStyle": "dark"
}
},
{
"name": "iPad - Landscape",
"options": {
"defaultTestExecutionTimeAllowance": 120,
"testTimeoutsEnabled": true
}
}
],
"defaultOptions": {
"targetForVariableExpansion": {
"containerPath": "container:MyApp.xcodeproj",
"identifier": "MyApp"
}
},
"testTargets": [
{
"target": {
"containerPath": "container:MyApp.xcodeproj",
"identifier": "MyAppUITests",
"name": "MyAppUITests"
}
}
],
"version": 1
}Configuration Options
配置选项
| Option | Purpose |
|---|---|
| Test localization |
| Test regional formatting |
| Test dark/light mode |
| App target for configuration |
| Enable timeout enforcement |
| Timeout in seconds |
| 选项 | 用途 |
|---|---|
| 测试本地化效果 |
| 测试区域格式 |
| 测试深色/浅色模式 |
| 用于配置的应用目标 |
| 启用超时强制机制 |
| 超时时间(秒) |
Running with Test Plan
使用测试计划运行测试
bash
undefinedbash
undefinedCommand line
命令行方式
xcodebuild test
-scheme "MyApp"
-testPlan "MyTestPlan"
-destination "platform=iOS Simulator,name=iPhone 16"
-resultBundlePath /tmp/results.xcresult
-scheme "MyApp"
-testPlan "MyTestPlan"
-destination "platform=iOS Simulator,name=iPhone 16"
-resultBundlePath /tmp/results.xcresult
xcodebuild test
-scheme "MyApp"
-testPlan "MyTestPlan"
-destination "platform=iOS Simulator,name=iPhone 16"
-resultBundlePath /tmp/results.xcresult
-scheme "MyApp"
-testPlan "MyTestPlan"
-destination "platform=iOS Simulator,name=iPhone 16"
-resultBundlePath /tmp/results.xcresult
In Xcode
Xcode中操作
Product → Test Plan → Select your plan
产品 → 测试计划 → 选择你的测试计划
Then Cmd+U to run tests
然后按Cmd+U运行测试
undefinedundefinedPhase 3: Review
阶段3:审查
Test Report Features
测试报告功能
After tests complete:
- View test results in Report Navigator
- Watch video recordings of each test
- See screenshots at failure points
- Analyze timeline of actions
测试完成后:
- 查看测试结果:在报告导航器中查看
- 观看测试视频录制:查看每个测试的操作视频
- 查看失败截图:查看测试失败时的截图
- 分析操作时间线:查看操作的时间线
Enabling Attachments
启用附件留存
In test plan or scheme:
json
"options": {
"systemAttachmentLifetime": "keepAlways",
"userAttachmentLifetime": "keepAlways"
}在测试计划或Scheme中配置:
json
"options": {
"systemAttachmentLifetime": "keepAlways",
"userAttachmentLifetime": "keepAlways"
}Capturing Custom Screenshots
捕获自定义截图
swift
func testCheckout() {
// ... actions ...
// Manual screenshot at specific point
let screenshot = app.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = "Checkout Confirmation"
attachment.lifetime = .keepAlways
add(attachment)
}swift
func testCheckout() {
// ... 执行操作 ...
// 在特定步骤手动截图
let screenshot = app.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = "Checkout Confirmation"
attachment.lifetime = .keepAlways
add(attachment)
}Common Patterns
常见模式
Login Flow Template
登录流模板
swift
func testLoginWithValidCredentials() throws {
let app = XCUIApplication()
app.launch()
// Navigate to login
let showLoginButton = app.buttons["showLoginButton"]
XCTAssertTrue(showLoginButton.waitForExistence(timeout: 5))
showLoginButton.tap()
// Enter credentials
let emailField = app.textFields["emailTextField"]
XCTAssertTrue(emailField.waitForExistence(timeout: 5))
emailField.tap()
emailField.typeText("test@example.com")
let passwordField = app.secureTextFields["passwordTextField"]
passwordField.tap()
passwordField.typeText("password123")
// Submit
app.buttons["loginButton"].tap()
// Verify success
let welcomeScreen = app.staticTexts["welcomeLabel"]
XCTAssertTrue(welcomeScreen.waitForExistence(timeout: 10))
}swift
func testLoginWithValidCredentials() throws {
let app = XCUIApplication()
app.launch()
// 导航到登录页面
let showLoginButton = app.buttons["showLoginButton"]
XCTAssertTrue(showLoginButton.waitForExistence(timeout: 5))
showLoginButton.tap()
// 输入凭证
let emailField = app.textFields["emailTextField"]
XCTAssertTrue(emailField.waitForExistence(timeout: 5))
emailField.tap()
emailField.typeText("test@example.com")
let passwordField = app.secureTextFields["passwordTextField"]
passwordField.tap()
passwordField.typeText("password123")
// 提交登录
app.buttons["loginButton"].tap()
// 验证登录成功
let welcomeScreen = app.staticTexts["welcomeLabel"]
XCTAssertTrue(welcomeScreen.waitForExistence(timeout: 10))
}Navigation Flow Template
导航流模板
swift
func testNavigateToSettings() throws {
let app = XCUIApplication()
app.launch()
// Open tab bar item
app.tabBars.buttons["Settings"].tap()
// Verify navigation
let settingsTitle = app.navigationBars["Settings"]
XCTAssertTrue(settingsTitle.waitForExistence(timeout: 5))
// Navigate deeper
app.tables.cells["Account"].tap()
XCTAssertTrue(app.navigationBars["Account"].exists)
}swift
func testNavigateToSettings() throws {
let app = XCUIApplication()
app.launch()
// 打开标签栏项
app.tabBars.buttons["Settings"].tap()
// 验证导航结果
let settingsTitle = app.navigationBars["Settings"]
XCTAssertTrue(settingsTitle.waitForExistence(timeout: 5))
// 深层导航
app.tables.cells["Account"].tap()
XCTAssertTrue(app.navigationBars["Account"].exists)
}Form Validation Template
表单验证模板
swift
func testFormValidation() throws {
let app = XCUIApplication()
app.launch()
// Submit empty form
app.buttons["submitButton"].tap()
// Verify error appears
let errorAlert = app.alerts["Error"]
XCTAssertTrue(errorAlert.waitForExistence(timeout: 5))
XCTAssertTrue(errorAlert.staticTexts["Please fill all fields"].exists)
// Dismiss alert
errorAlert.buttons["OK"].tap()
}swift
func testFormValidation() throws {
let app = XCUIApplication()
app.launch()
// 提交空表单
app.buttons["submitButton"].tap()
// 验证错误提示出现
let errorAlert = app.alerts["Error"]
XCTAssertTrue(errorAlert.waitForExistence(timeout: 5))
XCTAssertTrue(errorAlert.staticTexts["Please fill all fields"].exists)
// 关闭提示框
errorAlert.buttons["OK"].tap()
}Troubleshooting
故障排查
Recording Doesn't Start
录制无法启动
- Ensure you're in a test method
- Check simulator is available
- Verify app builds and runs
- Try restarting Xcode
- 确保光标位于测试方法内部
- 检查Simulator是否可用
- 验证应用可以正常构建和运行
- 尝试重启Xcode
Recorded Code Doesn't Work
录制代码无法运行
- Add waitForExistence before interactions
- Check accessibility identifiers are set
- Simplify queries to shortest form
- Run app manually to verify flow works
- 添加waitForExistence:在交互前等待元素加载
- 检查无障碍标识符:确认已正确设置
- 简化查询语句:使用最短的有效查询
- 手动运行应用:验证流程本身是否正常
Tests Pass Locally, Fail in CI
本地测试通过,CI环境测试失败
- Increase timeouts for slower CI machines
- Add explicit waits for animations
- Check simulator configuration matches
- Disable animations in test setup:
swift
app.launchArguments = ["--disable-animations"]
- 增加超时时间:针对CI机器性能较慢的情况
- 添加显式等待:等待动画完成
- 检查Simulator配置:确保与本地一致
- 禁用动画:在测试初始化中添加:
swift
app.launchArguments = ["--disable-animations"]
Anti-Patterns
反模式
Don't Use Raw Recorded Code in CI
不要在CI中使用原始录制代码
swift
// BAD - Raw recorded code
app.buttons["Login"].tap()
app.textFields["Email"].typeText("user@example.com")
// GOOD - Enhanced for CI
let loginButton = app.buttons["loginButton"]
XCTAssertTrue(loginButton.waitForExistence(timeout: 10))
loginButton.tap()swift
// 错误示例 - 原始录制代码
app.buttons["Login"].tap()
app.textFields["Email"].typeText("user@example.com")
// 正确示例 - 针对CI增强后的代码
let loginButton = app.buttons["loginButton"]
XCTAssertTrue(loginButton.waitForExistence(timeout: 10))
loginButton.tap()Don't Hardcode Coordinates
不要硬编码坐标
swift
// BAD - Coordinates from recording
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
// GOOD - Use element queries
app.buttons["centerButton"].tap()swift
// 错误示例 - 录制生成的坐标
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
// 正确示例 - 使用元素查询
app.buttons["centerButton"].tap()Don't Skip Assertions
不要跳过断言
swift
// BAD - Actions only
app.buttons["Login"].tap()
sleep(2) // Hope it works
// GOOD - Verify outcomes
app.buttons["loginButton"].tap()
XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 10))swift
// 错误示例 - 仅执行操作
app.buttons["Login"].tap()
sleep(2) // 依赖等待,不可靠
// 正确示例 - 验证执行结果
app.buttons["loginButton"].tap()
XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 10))Resources
资源
WWDC: 2025-344, 2024-10206, 2019-413
Docs: /xcode/testing/recording-ui-tests, /xctest/xcuiapplication
Skills: axiom-xctest-automation, axiom-ui-testing
WWDC:2025-344, 2024-10206, 2019-413
文档:/xcode/testing/recording-ui-tests, /xctest/xcuiapplication
技能:axiom-xctest-automation, axiom-ui-testing