axiom-ui-recording

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Recording 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

开始录制

  1. Open your UI test file in Xcode
  2. Place cursor inside a test method
  3. Debug → Record UI Automation (or use the record button)
  4. App launches in Simulator
  5. Perform interactions - Xcode generates code
  6. Stop recording when done
  1. 在Xcode中打开你的UI测试文件
  2. 将光标置于测试方法内部
  3. 调试 → 录制UI自动化(或使用录制按钮)
  4. 应用将在Simulator中启动
  5. 执行交互操作 - Xcode会生成对应代码
  6. 完成后停止录制

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:
ScenarioProblemSolution
Localized strings"Login" changes by languageUse accessibilityIdentifier
Deeply nested viewsLong query chains break easilyUse shortest possible query
Dynamic contentCell content changesUse identifier or generic query
Multiple matchesQuery returns many elementsAdd unique identifier
来自WWDC 2025-344:
场景问题解决方案
本地化字符串"Login"会随语言变化使用accessibilityIdentifier
深层嵌套视图长查询链容易失效使用尽可能短的查询语句
动态内容单元格内容会变化使用标识符或通用查询
多个匹配结果查询返回多个元素添加唯一标识符

Best Practices

最佳实践

  1. Prefer identifiers over labels
  2. Use the shortest query that works
  3. Avoid index-based queries (
    element(boundBy: 0)
    )
  4. Add identifiers to dynamic content
  1. 优先使用标识符而非标签
  2. 使用能生效的最短查询语句
  3. 避免基于索引的查询
    element(boundBy: 0)
  4. 为动态内容添加标识符

Phase 2: Replay with Test Plans

阶段2:使用测试计划重放

Test plans allow running the same tests across multiple configurations.
测试计划允许在多种配置下运行相同的测试。

Creating a Test Plan

创建测试计划

  1. File → New → File → Test Plan
  2. Add test targets
  3. Configure configurations
  1. 文件 → 新建 → 文件 → 测试计划
  2. 添加测试目标
  3. 配置各项参数

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

配置选项

OptionPurpose
language
Test localization
region
Test regional formatting
userInterfaceStyle
Test dark/light mode
targetForVariableExpansion
App target for configuration
testTimeoutsEnabled
Enable timeout enforcement
defaultTestExecutionTimeAllowance
Timeout in seconds
选项用途
language
测试本地化效果
region
测试区域格式
userInterfaceStyle
测试深色/浅色模式
targetForVariableExpansion
用于配置的应用目标
testTimeoutsEnabled
启用超时强制机制
defaultTestExecutionTimeAllowance
超时时间(秒)

Running with Test Plan

使用测试计划运行测试

bash
undefined
bash
undefined

Command line

命令行方式

xcodebuild test
-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

In Xcode

Xcode中操作

Product → Test Plan → Select your plan

产品 → 测试计划 → 选择你的测试计划

Then Cmd+U to run tests

然后按Cmd+U运行测试

undefined
undefined

Phase 3: Review

阶段3:审查

Test Report Features

测试报告功能

After tests complete:
  1. View test results in Report Navigator
  2. Watch video recordings of each test
  3. See screenshots at failure points
  4. Analyze timeline of actions
测试完成后:
  1. 查看测试结果:在报告导航器中查看
  2. 观看测试视频录制:查看每个测试的操作视频
  3. 查看失败截图:查看测试失败时的截图
  4. 分析操作时间线:查看操作的时间线

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

录制无法启动

  1. Ensure you're in a test method
  2. Check simulator is available
  3. Verify app builds and runs
  4. Try restarting Xcode
  1. 确保光标位于测试方法内部
  2. 检查Simulator是否可用
  3. 验证应用可以正常构建和运行
  4. 尝试重启Xcode

Recorded Code Doesn't Work

录制代码无法运行

  1. Add waitForExistence before interactions
  2. Check accessibility identifiers are set
  3. Simplify queries to shortest form
  4. Run app manually to verify flow works
  1. 添加waitForExistence:在交互前等待元素加载
  2. 检查无障碍标识符:确认已正确设置
  3. 简化查询语句:使用最短的有效查询
  4. 手动运行应用:验证流程本身是否正常

Tests Pass Locally, Fail in CI

本地测试通过,CI环境测试失败

  1. Increase timeouts for slower CI machines
  2. Add explicit waits for animations
  3. Check simulator configuration matches
  4. Disable animations in test setup:
    swift
    app.launchArguments = ["--disable-animations"]
  1. 增加超时时间:针对CI机器性能较慢的情况
  2. 添加显式等待:等待动画完成
  3. 检查Simulator配置:确保与本地一致
  4. 禁用动画:在测试初始化中添加:
    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