mcpField
A
axiom-ui-testing

axiom-ui-testing

github.com/charleswiltgen测试与质量1.2k199 次安装今天更新
代码仓库

关于 axiom-ui-testing

面向编写 Apple 平台 UI 测试的开发者,提供减少时序不稳定问题的实践指南。内容涵盖基于条件的等待、无障碍标识符、多设备测试,以及 Xcode 26 的 UI 自动化录制与视频调试。

主要功能

  1. 01用条件等待替代固定延时
  2. 02等待元素出现、消失及状态变化
  3. 03使用稳定的无障碍标识符
  4. 04跨设备与语言回放 UI 测试
  5. 05查看录制视频并调试失败

适用场景

  1. 01排查本地通过但 CI 失败的测试
  2. 02替换测试中的 sleep() 延时
  3. 03定位 UI 自动化录制中的失败步骤
  4. 04让测试适配不同设备尺寸

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-ui-testing

安装后在 Claude Code 中输入/axiom-ui-testing确认已加载。

详细介绍

UI 测试

概述

等待条件,而不是任意时长。核心原则:测试不稳定,源于对操作耗时的猜测。基于条件的等待可以消除竞态条件。

WWDC 2025 新功能:录制 UI 自动化支持录制交互、在不同设备和语言间重放,并查看测试运行的视频录像。

示例提示词

以下是开发者实际提出的问题,本技能旨在解答这些问题:

1. “我的 UI 测试在 Mac 上本地运行通过,但在 CI 中失败。如何提高测试的可靠性?”

→ 本技能展示了适用于不同设备和运行速度的基于条件的等待模式,消除了 CI 中的计时差异

2. “我的测试用了 sleep(2) 和 sleep(5),但还是不稳定。如何用真实条件替代任意超时?”

→ 本技能演示了用于等待数据加载、网络请求和动画的 waitForExistence、XCTestExpectation 和轮询模式

3. “我刚用 Xcode 26 的录制 UI 自动化录制了一项测试。如何查看视频并调试失败?”

→ 本技能介绍了视频调试工作流,用于分析录像并找出测试失败的确切步骤

4. “我的测试在 iPad 上失败,在 iPhone 上却通过了。如何编写适用于各种设备尺寸的测试?”

→ 本技能介绍了多因素测试策略和与设备无关的谓词,助你实现可靠的跨设备测试

5. “我想编写稳定可靠的测试。需要了解哪些关键模式?”

→ 本技能提供了基于条件的等待模板、无障碍优先模式,以及构建可靠测试架构的决策树


危险信号 — 测试可靠性问题

如果出现以下任意情况,请留意计时问题:

  • 测试在本地通过,在 CI 中失败(计时差异)
  • 测试时而通过、时而失败(竞态条件)
  • 测试使用 sleep() 或 Thread.sleep()(任意延迟)
  • 测试因“未找到 UI 元素”而失败,重试后却通过
  • 测试运行时间过长(等待最坏情况)

快速决策树

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

核心模式:基于条件的等待

❌ 错误(任意超时):

func testButtonAppears() {
    app.buttons["Login"].tap()
    sleep(2)  // ❌ Guessing it takes 2 seconds
    XCTAssertTrue(app.buttons["Dashboard"].exists)
}

✅ 正确(等待条件):

func testButtonAppears() {
    app.buttons["Login"].tap()
    let dashboard = app.buttons["Dashboard"]
    XCTAssertTrue(dashboard.waitForExistence(timeout: 5))
}

常见 UI 测试模式

模式 1:等待元素
// Wait for element to appear
func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool {
    return element.waitForExistence(timeout: timeout)
}

// Usage
XCTAssertTrue(waitForElement(app.buttons["Submit"]))
模式 2:等待元素消失
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"]))
模式 3:等待特定状态
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()
模式 4:无障碍标识符

在应用中设置:

Button("Submit") {
    // action
}
.accessibilityIdentifier("submitButton")

在测试中使用:

func testSubmitButton() {
    let submitButton = app.buttons["submitButton"]  // Uses identifier, not label
    XCTAssertTrue(submitButton.waitForExistence(timeout: 5))
    submitButton.tap()
}

原因:无障碍标识符不会随本地化而改变,在 UI 更新时也能保持稳定。

模式 5:网络请求延迟
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)
}
模式 6:动画处理
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))
}

测试清单

编写测试前
  • 为所有可交互元素使用无障碍标识符
  • 避免使用硬编码标签(改用标识符)
  • 为网络延迟和动画做好准备
  • 选择合适的超时时长(UI 为 2s,网络为 10s)
编写测试时
  • 使用 waitForExistence(),不要使用 sleep()
  • 对复杂条件使用谓词
  • 测试成功和失败路径
  • 确保测试彼此独立(可按任意顺序运行)
编写测试后
  • 在本地运行测试 10 次(检查是否不稳定)
  • 在支持的最慢设备上运行测试
  • 在 CI 环境中运行测试
  • 检查测试耗时(如果每项测试超过 30s,请进行优化)

Xcode UI 测试技巧

用于测试的启动参数
func testExample() {
    let app = XCUIApplication()
    app.launchArguments = ["UI-Testing"]
    app.launch()
}

在应用代码中:

if ProcessInfo.processInfo.arguments.contains("UI-Testing") {
    // Use mock data, skip onboarding, etc.
}
加快测试执行
override func setUpWithError() throws {
    continueAfterFailure = false  // Stop on first failure
}
调试失败的测试
func testExample() {
    // Take screenshot on failure
    addUIInterruptionMonitor(withDescription: "Alert") { alert in
        alert.buttons["OK"].tap()
        return true
    }

    // Print element hierarchy
    print(app.debugDescription)
}

常见错误

❌ 所有情况都使用 sleep()
sleep(5)  // ❌ Wastes time if operation completes in 1s
❌ 未处理动画
app.buttons["Next"].tap()
XCTAssertTrue(app.buttons["Back"].exists)  // ❌ May fail during animation
❌ 使用硬编码文本标签
app.buttons["Submit"].tap()  // ❌ Breaks with localization
❌ 测试之间存在依赖
// ❌ Test 2 assumes Test 1 ran first
func test1_Login() { /* ... */ }
func test2_ViewDashboard() { /* assumes logged in */ }
❌ 没有超时策略
element.waitForExistence(timeout: 100)  // ❌ Too long
element.waitForExistence(timeout: 0.1)  // ❌ Too short

使用合适的超时时长:

  • UI 动画:2-3 秒
  • 网络请求:10 秒
  • 复杂操作:最多 30 秒

实际效果

之前(使用 sleep()):

  • 测试套件:15 分钟(等待最坏情况)
  • 不稳定测试:失败率为 20%
  • CI 失败:50% 需要重试

之后(基于条件等待):

  • 测试套件:5 分钟(仅在需要时等待)
  • 不稳定测试:失败率低于 2%
  • CI 失败:少于 5% 需要重试

关键见解:等待实际条件,而不是猜测时间,测试完成得更快,也更可靠。


录制 UI 自动化

概述

Xcode 26 新增:录制、重放和查看 UI 自动化测试视频。

三个阶段:

  1. 录制 — 将交互(轻点、滑动、硬件按钮按下)捕获为 Swift 代码
  2. 重放 — 在多种设备、语言、地区和方向上运行
  3. 查看 — 观看视频录制、分析失败原因、查看 UI 元素叠加层

支持的平台:iOS、iPadOS、macOS、watchOS、tvOS、axiom-visionOS(为 iPad 设计)

UI 自动化的工作原理

核心原则:

  • UI 自动化会像用户一样通过手势和硬件事件与应用交互
  • 完全独立于应用运行(无法直接访问应用模型/数据)
  • 使用辅助功能框架作为底层技术
  • 告知操作系统要执行哪些手势,然后依次同步等待每个手势完成

操作包括:

  • 启动应用
  • 与按钮和导航交互
  • 设置系统状态(深色模式、axiom-localization 等)
  • 设置模拟位置
辅助功能是基础

重要理解:辅助功能会直接向 UI 自动化提供信息。

辅助功能可识别的内容:

  • 元素类型(按钮、文本、图像等)
  • 标签(可见文本)
  • 值(复选框的当前状态等)
  • 框架(元素位置)
  • 标识符(辅助功能标识符——不本地化)

最佳实践:出色的辅助功能体验 = 出色的 UI 自动化体验。

为录制准备应用
步骤 1:添加辅助功能标识符

SwiftUI:

Button("Submit") {
    // action
}
.accessibilityIdentifier("submitButton")

// Make identifiers specific to instance
List(landmarks) { landmark in
    LandmarkRow(landmark)
        .accessibilityIdentifier("landmark-\(landmark.id)")
}

UIKit:

let button = UIButton()
button.accessibilityIdentifier = "submitButton"

// Use index for table cells
cell.accessibilityIdentifier = "cell-\(indexPath.row)"

好的标识符应当:

  • ✅ 在整个应用中唯一
  • ✅ 能描述元素内容
  • ✅ 静态(不随内容变化而变化)
  • ✅ 不本地化(在不同语言中保持一致)

标识符的重要性:

  • 标题/描述可能会变化,而标识符保持稳定
  • 可跨本地化字符串使用
  • 可唯一标识包含动态内容的元素

专业提示:使用 Xcode 编码助手添加标识符:

Prompt: "Add accessibility identifiers to the relevant parts of this view"
步骤 2:使用辅助功能检查器检查辅助功能

启动辅助功能检查器:

  • Xcode 菜单 → 打开开发者工具 → 辅助功能检查器
  • 或者:通过 Spotlight 启动

功能:

  1. 元素检查器 — 列出任意视图的辅助功能值
  2. 属性详情 — 点击属性名称查看文档
  3. 平台支持 — 适用于所有 Apple 平台

检查内容:

  • 元素是否具有标签
  • 交互元素是否具有类型(例如按钮,而不只是文本)
  • 有状态元素(复选框、切换开关)是否设置了值
  • 包含动态/本地化内容的元素是否设置了标识符

示例代码参考:打造出色的辅助功能体验

步骤 3:添加 UI 测试目标
  1. 在 Xcode 中打开项目设置
  2. 点击目标列表下方的“+”
  3. 选择 UI Testing Bundle
  4. 点击“完成”

结果:项目中会新增一个包含模板测试的 UI 测试文件夹。

录制交互
开始录制(Xcode 26)
  1. 打开 UI 测试源文件
  2. 弹出说明框,介绍如何开始录制(仅首次显示)
  3. 点击编辑器边栏中的 “Start Recording” 按钮
  4. Xcode 会构建应用,并在模拟器/设备上启动应用

录制期间:

  • 像平常一样与应用交互(轻点、滑动、输入文本等)
  • 编辑器会实时显示代表交互的代码
  • 录制内容会随着输入更新(例如文本字段中的输入)

停止录制:

  • 点击 Xcode 中的 “Stop Run” 按钮
录制会话示例
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()
}
检查录制的代码

录制后,请检查并调整查询:

多个选项:每行都有下拉菜单,可显示定位元素的其他方式。

选择建议:

  1. 对于本地化字符串(文本、按钮标签):如果有辅助功能标识符,请选择它
  2. 对于深层嵌套的视图:选择最短的查询(应用发生变化时依然稳健)
  3. 对于动态内容(时间戳、温度):使用通用查询或标识符

示例:

// 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, shortest

请根据需要选择最短、最稳定的查询。

添加验证

录制后,添加断言以验证预期行为:

等待元素出现
// Validate collection created
let collection = app.buttons["Max's Australian Adventure"]
XCTAssertTrue(collection.waitForExistence(timeout: 5))
等待属性变化
// Wait for button to become enabled
let submitButton = app.buttons["Submit"]
XCTAssertTrue(submitButton.wait(for: .enabled, toEqual: true, timeout: 5))
与 XCTAssert 结合使用
// Fail test if element doesn't appear
let landmark = app.staticTexts["Great Barrier Reef"]
XCTAssertTrue(landmark.waitForExistence(timeout: 5), "Landmark should appear in collection")
高级自动化 API
设置设备状态
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()
}
启动参数和环境变量
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()
}

在应用代码中:

if ProcessInfo.processInfo.arguments.contains("-UI-Testing") {
    // Use mock data, skip onboarding
}
自定义 URL 方案
// 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")!)
在测试中执行辅助功能审计
func testAccessibility() throws {
    let app = XCUIApplication()
    app.launch()

    // Perform accessibility audit
    try app.performAccessibilityAudit()
}

参考:为应用执行辅助功能审计 — WWDC23

针对多种配置的测试计划

测试计划可用于:

  • 包含/排除单个测试
  • 设置系统选项(语言、地区、外观)
  • 配置测试属性(超时时间、重复次数、并行化)
  • 关联方案,以指定构建设置
创建测试计划
  1. 创建新的测试计划,或使用现有计划
  2. 在第一个屏幕中添加/移除测试
  3. 切换到 Configurations 标签页
添加多种语言
Configurations:
├─ English
├─ German (longer strings)
├─ Arabic (right-to-left)
└─ Hebrew (right-to-left)

每个区域设置 = 测试计划中的单独配置。

设置:

  • 针对特定区域设置
  • 在所有配置间共享
视频和屏幕快照捕获

在 Configurations 标签页中:

  • Capture screenshots:开/关
  • Capture video:开/关
  • Keep media:“Only failures”或“On, and keep all”

默认值:仅保留失败运行的视频/屏幕快照(供检查)。

“On, and keep all”的使用场景:

  • 文档
  • 教程
  • 营销材料

参考:为 Xcode Cloud 编写快速可靠的测试 — WWDC22

在 Xcode Cloud 中重新运行测试

Xcode Cloud = 内置服务,可用于:

  • 构建应用
  • 运行测试
  • 上传到 App Store
  • 完全在云端执行,无需使用团队设备

工作流配置:

  • 使用与本地相同的测试计划
  • 在多种设备和配置上运行
  • 在 App Store Connect 中查看视频/结果

查看结果:

  • Xcode:Xcode Cloud 部分
  • App Store Connect:Xcode Cloud 部分
  • 查看构建信息、日志、失败说明和视频录制

团队访问权限:整个团队都可以查看运行历史记录并下载结果/视频。

参考:在 Xcode Cloud 中创建实用工作流 — WWDC23

查看带视频的测试结果
访问测试报告
  1. 点击 Xcode 中的 Test 按钮
  2. 双击失败的运行,即可查看视频和说明

功能:

  • Runs 下拉菜单 — 在不同配置(语言、设备)的录制视频间切换
  • 保存视频 — 点按辅助键 → 保存
  • 播放/暂停 — 播放视频,并显示 UI 交互叠加层
  • 时间线标记点 — UI 交互以标记点的形式显示在时间线上
  • 跳转到失败处 — 点击时间线上的失败菱形标记
失败时的 UI 元素叠加层

失败时:

  • 点击时间线上的失败点
  • 叠加层会显示屏幕上存在的所有 UI 元素
  • 点击任意元素,查看解决该问题的代码建议
  • 显示全部 — 查看其他示例

工作流:

  1. 确认实际存在的内容(与测试预期的内容对比)
  2. 点击元素以获取查询代码
  3. 次要点击 → 复制代码
  4. 查看源代码 → 直接跳转到测试
  5. 粘贴修正后的代码

示例:

// 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
使用其他语言运行测试

点击测试菱形 → 选择配置(例如阿拉伯语)→ 观察自动化在从右向左的布局中运行。

验证:同一自动化能否在不同语言和布局中正常工作。

参考:使用 Xcode 测试报告更快地修复失败 — WWDC23

UI 自动化录制检查清单
录制前
  • 为交互元素添加辅助功能标识符
  • 使用辅助功能检查器检查应用
  • 向项目添加 UI 测试 Bundle 目标
  • 规划要录制的工作流(用户旅程)
录制期间
  • 自然地与应用交互
  • 录制完整的用户旅程(而非单次点击)
  • 检查代码是否随交互生成
  • 工作流完成后停止录制
录制后
  • 检查录制生成的代码选项(每行的下拉菜单)
  • 选择稳定的查询方式(标识符 > 标签)
  • 添加验证(waitForExistence、XCTAssert)
  • 添加设置代码(设备状态、启动参数)
  • 运行测试,确认其通过
测试计划配置
  • 创建或更新测试计划
  • 添加多种语言配置
  • 包含从右向左书写的语言(阿拉伯语、希伯来语)
  • 配置视频/屏幕截图捕获设置
  • 为网络测试设置合适的超时时间
运行和检查
  • 在本地跨不同配置运行测试
  • 检查视频录制以排查失败
  • 使用 UI 元素叠加层调试失败
  • 在 Xcode Cloud 中运行,以便团队查看
  • 如有需要,下载并分享视频

测试中的网络调节

概述

UI 测试在高速网络下可能通过,但在 3G/LTE 下可能失败。Network Link Conditioner 可模拟真实网络状况,从而捕获对时序敏感的崩溃。

关键场景:

  • ❌ iPad Pro 使用 Wi-Fi(高速)→ 通过
  • ❌ iPad Pro 使用 3G(低速)→ 崩溃
  • ✅ 在两种网络下测试,以捕获设备特定的故障
设置 Network Link Conditioner

安装 Network Link Conditioner:

  1. 从 Apple 的 Xcode 附加工具下载
  2. 搜索:“Network Link Conditioner”
  3. 安装:sudo open Network\ Link\ Conditioner.pkg

验证安装:

# Check if installed
ls ~/Library/Application\ Support/Network\ Link\ Conditioner/

在测试中启用:

override func setUpWithError() throws {
    let app = XCUIApplication()

    // Launch with network conditioning argument
    app.launchArguments = ["-com.apple.CoreSimulator.CoreSimulatorService", "-networkShaping"]
    app.launch()
}
常见网络配置

3G 配置文件(大多数故障都发生在此配置下):

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()
}

手动调节网络(macOS“系统设置”):

  1. 打开“系统设置”→“网络”
  2. 点击“Network Link Conditioner”(已在上方安装)
  3. 选择配置文件:3G、LTE、WiFi
  4. 点击“开始”
  5. 运行测试(测试将使用限速网络)
真实示例:网络限速下的照片上传

❌ 未进行网络调节:

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

✅ 进行网络调节后:

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")
}

主要区别:

  • 超时时间更长(30 秒而非 5 秒)
  • 检查是否发生崩溃
  • 在预期的最慢网络下运行

多因素测试:设备尺寸 + 网络速度

问题

由于布局差异和网络延迟,测试可能在设备 A 上通过,却在设备 B 上失败。多因素测试可以捕获这些组合情况。

常见失败模式:

  • ✅ iPhone 14 Pro(紧凑,网络快)
  • ❌ iPad Pro 12.9(大屏,3G 网络)→ 崩溃
  • ✅ iPhone 15(紧凑,LTE)
  • ❌ iPhone 12(GPU 较旧,3G)→ 超时
多设备测试计划配置

在 Xcode 中创建测试计划:

  1. 文件 → 新建 → 测试计划
  2. 选择要包含的测试
  3. 点击“Configurations”选项卡
  4. 为每种设备/网络组合添加配置

配置矩阵示例:

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)

在测试计划 UI 中:

  • 设备:iPhone 14 Pro / iPad Pro 12.9
  • OS 版本:最新版本
  • 语言区域:英语
  • 网络配置:LTE / 3G
以编程方式进行设备专项测试
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)
    }
}
真实案例:iPad Pro + 3G 崩溃

场景:应用在 iPhone 14 上运行正常,但在 iPad Pro 上通过 3G 网络运行时崩溃。

崩溃原因:

  1. iPad Pro 的布局更大(横屏)
  2. 3G 网络速度较慢(延迟超过 100ms)
  3. 图片未能及时加载,导致布局引擎崩溃
  4. 单设备测试会遗漏这种组合

能捕获此问题的测试:

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")
}
在 CI 中运行多因素测试

在 GitHub Actions 或 Xcode Cloud 中:

- 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 之前捕获设备专项崩溃。


调试 UI 测试揭示的崩溃

概述

UI 测试有时会揭示手动测试中不会出现的崩溃。关键要点:自动化测试运行更快,与应用的交互方式不同,因此可能暴露并发/时序错误。

崩溃发生时:

  • ❌ 手动测试:无法复现(手动运行时正常)
  • ✅ UI 测试:每次都会崩溃(自动重复运行能发现竞态条件)
识别测试揭示的崩溃

测试输出中的迹象:

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]

视频显示:应用明显崩溃(黑屏,立即终止)。

系统化调试方法
第 1 步:捕获崩溃详情

启用详细日志:

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()
    }
}
第 2 步:在本地复现
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
}

运行测试并查看 Console 日志:

  • Xcode:视图 → 导航器 → 显示控制台
  • 留意异常消息
第 3 步:分析崩溃日志

位置:

  1. Xcode Console(实时显示,详细程度较低)
  2. ~/Library/Logs/DiagnosticMessages/crash_*.log(完整详情)
  3. 设备“设置” → 隐私 → 分析与改进 → 分析数据

查找以下信息:

  • 崩溃的线程
  • 异常类型(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 调用
第 4 步:关联 Swift 并发问题

大多数 UI 测试崩溃都属于并发错误(并非 UI 测试特有)。请参考相关技能:

// 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
            }
        }
    }
}

深入分析崩溃:有关 @MainActor 模式,请参阅 axiom-swift-concurrency 技能;有关线程安全问题,请参阅 axiom-memory-debugging 技能。

第 5 步:添加防崩溃测试

修复后:

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)
}
第 6 步:进行压力测试以验证修复
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")
}
预防检查清单
发布前
  • 在最慢的网络(3G)上运行 UI 测试
  • 在最大设备(iPad Pro)上运行测试
  • 在支持的最旧设备(iPhone 12)上运行测试
  • 录制测试运行视频(可节省调试时间)
  • 检查日志中是否有崩溃
  • 运行压力测试(重复操作 10 次)
  • 确认 UI 属性使用了 @MainActor
  • 检查异步代码中是否存在竞态条件

资源

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 了解变更

评论

支持 Markdown