关于 axiom-ui-recording
面向编写 UI 测试的开发者,介绍 Xcode 26 的 UI Automation 录制功能。内容涵盖在 Simulator 中录制交互、增强生成的测试、通过测试计划运行多种配置并检查结果。
主要功能
- 01将应用交互录制为 Swift 测试
- 02用标识符和等待机制增强生成测试
- 03通过测试计划配置运行测试
- 04查看视频、截图和操作时间线
适用场景
- 01根据应用交互创建 UI 测试
- 02稳定用于 CI 的录制测试
- 03测试本地化和外观配置
- 04调查 UI 测试失败
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-ui-recording安装后在 Claude Code 中输入/axiom-ui-recording确认已加载。
详细介绍
录制 UI 自动化(Xcode 26+)
Xcode 26 的 UI 自动化录制功能指南:通过录制用户交互来创建 UI 测试。
三阶段工作流
摘自 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 │
│ │
└─────────────────────────────────────────────────────────────┘
阶段 1:录制
开始录制
- 在 Xcode 中打开 UI 测试文件
- 将光标置于测试方法内
- Debug → Record UI Automation(或使用录制按钮)
- App 会在 Simulator 中启动
- 执行交互操作,Xcode 会生成代码
- 完成后停止录制
录制内容
- 按钮、单元格和控件上的轻点
- 在文本字段中输入文本
- 轻扫和滚动
- 手势(捏合、旋转)
- 硬件按钮按压(Home、音量)
生成的代码示例
// 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("[email protected]")
// Recorded: Tap password field, type password
app.secureTextFields["Password"].tap()
app.secureTextFields["Password"].typeText("password123")
// Recorded: Tap login button
app.buttons["Login"].tap()
}
改进录制的代码
重要:录制的代码往往不够稳定。务必进行改进,以提高稳定性。
1. 添加辅助功能标识符
录制的代码使用标签,在本地化后可能失效:
// RECORDED (fragile - breaks with localization)
app.buttons["Login"].tap()
// ENHANCED (stable - uses identifier)
app.buttons["loginButton"].tap()
在 App 代码中添加标识符:
// SwiftUI
Button("Login") { ... }
.accessibilityIdentifier("loginButton")
// UIKit
loginButton.accessibilityIdentifier = "loginButton"
2. 添加 waitForExistence
录制的代码假定元素会立即出现:
// 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()
3. 添加断言
录制的代码只执行操作,不进行验证:
// 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")
4. 使用更短的查询
录制的代码可能包含过于具体的查询:
// RECORDED (too specific)
app.tables.cells.element(boundBy: 0).buttons["Action"].tap()
// ENHANCED (simpler)
app.buttons["actionButton"].tap()
查询选择指南
摘自 WWDC 2025-344:
| 场景 | 问题 | 解决方案 |
|---|---|---|
| 本地化字符串 | “Login” 会随语言变化 | 使用 accessibilityIdentifier |
| 深层嵌套视图 | 长查询链容易失效 | 使用尽可能短的查询 |
| 动态内容 | 单元格内容会变化 | 使用标识符或通用查询 |
| 匹配结果过多 | 查询返回许多元素 | 添加唯一标识符 |
最佳实践
- 优先使用标识符,而非标签
- 使用能够正常工作的最短查询
- 避免基于索引的查询(
element(boundBy: 0)) - 为动态内容添加标识符
阶段 2:使用测试计划重放
测试计划可让你在多种配置下运行相同的测试。
创建测试计划
- File → New → File → Test Plan
- 添加测试目标
- 配置各项设置
测试计划结构
{
"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
}
配置选项
| 选项 | 用途 |
|---|---|
language | 测试本地化 |
region | 测试区域格式 |
userInterfaceStyle | 测试深色/浅色模式 |
targetForVariableExpansion | 用于配置的 App 目标 |
testTimeoutsEnabled | 启用超时限制 |
defaultTestExecutionTimeAllowance | 超时时间(秒) |
使用测试计划运行
# Command line
xcodebuild test \
-scheme "MyApp" \
-testPlan "MyTestPlan" \
-destination "platform=iOS Simulator,name=iPhone 16" \
-resultBundlePath /tmp/results.xcresult
# In Xcode
# Product → Test Plan → Select your plan
# Then Cmd+U to run tests
阶段 3:审查
测试报告功能
测试完成后:
- 在 Report Navigator 中查看测试结果
- 观看每项测试的录制视频
- 查看失败时的屏幕截图
- 分析操作的时间线
启用附件
在测试计划或方案中:
"options": {
"systemAttachmentLifetime": "keepAlways",
"userAttachmentLifetime": "keepAlways"
}
捕获自定义屏幕截图
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)
}
常见模式
登录流程模板
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("[email protected]")
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))
}
导航流程模板
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)
}
表单验证模板
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()
}
故障排查
无法开始录制
- 确保当前位于测试方法中
- 检查 Simulator 是否可用
- 确认 App 能够构建并运行
- 尝试重启 Xcode
录制的代码无法运行
- 在交互操作前添加 waitForExistence
- 检查是否设置了辅助功能标识符
- 简化查询,使用最短形式
- 手动运行 App,确认流程正常
测试在本地通过,在 CI 中失败
- 为运行较慢的 CI 机器增加超时时间
- 为动画添加显式等待
- 检查 Simulator 配置是否一致
- 在测试设置中停用动画:
app.launchArguments = ["--disable-animations"]
反模式
不要在 CI 中使用未经修改的录制代码
// BAD - Raw recorded code
app.buttons["Login"].tap()
app.textFields["Email"].typeText("[email protected]")
// GOOD - Enhanced for CI
let loginButton = app.buttons["loginButton"]
XCTAssertTrue(loginButton.waitForExistence(timeout: 10))
loginButton.tap()
不要硬编码坐标
// BAD - Coordinates from recording
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
// GOOD - Use element queries
app.buttons["centerButton"].tap()
不要跳过断言
// 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))
资源
WWDC:2025-344、2024-10206、2019-413
文档:/xcode/testing/recording-ui-tests、/xctest/xcuiapplication
技能:axiom-xctest-automation、axiom-ui-testing
评论
支持 Markdown