mcpField
A
axiom-xctest-automation

axiom-xctest-automation

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

关于 axiom-xctest-automation

axiom-xctest-automation 是供开发者编写可维护 XCUITest 界面测试的指南,重点包括稳定的元素标识、条件等待和测试隔离,并提供运行与调试示例。

主要功能

  1. 01使用 accessibilityIdentifier 稳定查询
  2. 02建议条件等待而非 sleep
  3. 03组织测试设置与独立用例
  4. 04涵盖常见界面交互和测试计划
  5. 05包含 CI 执行与失败调试

适用场景

  1. 01编写可靠的登录界面测试
  2. 02等待异步界面变化
  3. 03跨配置运行测试
  4. 04通过截图或层级输出排查失败

安装

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

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-xctest-automation

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

详细介绍

XCUITest 自动化模式

使用 XCUITest 编写可靠、易于维护的 UI 测试的全面指南。

核心原则

可靠的 UI 测试需要具备以下三点:

  1. 稳定的元素标识(accessibilityIdentifier)
  2. 基于条件的等待(绝不使用硬编码的 sleep)
  3. 清晰的测试隔离(不共享状态)

元素标识

无障碍标识符模式

务必为测试关键元素使用 accessibilityIdentifier。

// SwiftUI
Button("Login") { ... }
    .accessibilityIdentifier("loginButton")

TextField("Email", text: $email)
    .accessibilityIdentifier("emailTextField")

// UIKit
loginButton.accessibilityIdentifier = "loginButton"
emailTextField.accessibilityIdentifier = "emailTextField"
查询选择指南

摘自 WWDC 2025-344《录制 UI 自动化》:

  1. 本地化字符串会变化 → 改用 accessibilityIdentifier
  2. 视图嵌套层级较深 → 使用尽可能简短的查询
  3. 动态内容 → 使用通用查询或标识符
// BAD - Fragile queries
app.buttons["Login"]  // Breaks with localization
app.tables.cells.element(boundBy: 0).buttons.firstMatch  // Too specific

// GOOD - Stable queries
app.buttons["loginButton"]  // Uses identifier
app.tables.cells.containing(.staticText, identifier: "itemTitle").firstMatch

等待策略

切勿使用 sleep()
// BAD - Hardcoded wait
sleep(5)
XCTAssertTrue(app.buttons["submit"].exists)

// GOOD - Condition-based wait
let submitButton = app.buttons["submit"]
XCTAssertTrue(submitButton.waitForExistence(timeout: 5))
等待模式
// Wait for element to appear
func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 10) -> Bool {
    element.waitForExistence(timeout: timeout)
}

// Wait for element to disappear
func waitForElementToDisappear(_ element: XCUIElement, timeout: TimeInterval = 10) -> 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
}

// Wait for element to be hittable (visible AND enabled)
func waitForElementHittable(_ element: XCUIElement, timeout: TimeInterval = 10) -> Bool {
    let predicate = NSPredicate(format: "isHittable == true")
    let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
    let result = XCTWaiter.wait(for: [expectation], timeout: timeout)
    return result == .completed
}

// Wait for text to appear anywhere
func waitForText(_ text: String, timeout: TimeInterval = 10) -> Bool {
    app.staticTexts[text].waitForExistence(timeout: timeout)
}
异步操作
// Wait for network response
func waitForNetworkResponse() {
    let loadingIndicator = app.activityIndicators["loadingIndicator"]

    // Wait for loading to start
    _ = loadingIndicator.waitForExistence(timeout: 5)

    // Wait for loading to finish
    _ = waitForElementToDisappear(loadingIndicator, timeout: 30)
}

测试结构

设置和清理
class LoginTests: XCTestCase {
    var app: XCUIApplication!

    override func setUpWithError() throws {
        continueAfterFailure = false
        app = XCUIApplication()

        // Reset app state for clean test
        app.launchArguments = ["--uitesting", "--reset-state"]
        app.launchEnvironment = ["DISABLE_ANIMATIONS": "1"]
        app.launch()
    }

    override func tearDownWithError() throws {
        // Capture screenshot on failure
        if testRun?.failureCount ?? 0 > 0 {
            let screenshot = XCUIScreen.main.screenshot()
            let attachment = XCTAttachment(screenshot: screenshot)
            attachment.name = "Failure Screenshot"
            attachment.lifetime = .keepAlways
            add(attachment)
        }
        app.terminate()
    }
}
测试方法模式
func testLoginWithValidCredentials() throws {
    // ARRANGE - Navigate to login screen
    let loginButton = app.buttons["showLoginButton"]
    XCTAssertTrue(loginButton.waitForExistence(timeout: 5))
    loginButton.tap()

    // ACT - Enter credentials and submit
    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")

    app.buttons["loginSubmitButton"].tap()

    // ASSERT - Verify successful login
    let welcomeLabel = app.staticTexts["welcomeLabel"]
    XCTAssertTrue(welcomeLabel.waitForExistence(timeout: 10))
    XCTAssertTrue(welcomeLabel.label.contains("Welcome"))
}

常见交互

文本输入
// Clear and type
let textField = app.textFields["emailTextField"]
textField.tap()
textField.clearText()  // Custom extension
textField.typeText("[email protected]")

// Extension to clear text
extension XCUIElement {
    func clearText() {
        guard let stringValue = value as? String else { return }
        tap()
        let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count)
        typeText(deleteString)
    }
}
滚动
// Scroll until element is visible
func scrollToElement(_ element: XCUIElement, in scrollView: XCUIElement) {
    while !element.isHittable {
        scrollView.swipeUp()
    }
}

// Scroll to specific element
let targetCell = app.tables.cells["targetItem"]
let table = app.tables.firstMatch
scrollToElement(targetCell, in: table)
targetCell.tap()
警告框和工作表
// Handle system alert
addUIInterruptionMonitor(withDescription: "Permission Alert") { alert in
    if alert.buttons["Allow"].exists {
        alert.buttons["Allow"].tap()
        return true
    }
    return false
}
app.tap() // Trigger the monitor

// Handle app alert
let alert = app.alerts["Error"]
if alert.waitForExistence(timeout: 5) {
    alert.buttons["OK"].tap()
}
收起键盘
// Dismiss keyboard
if app.keyboards.count > 0 {
    app.toolbars.buttons["Done"].tap()
    // Or tap outside
    // app.tap()
}

测试计划

多配置测试

测试计划支持使用不同配置运行相同的测试:


{
  "configurations" : [
    {
      "name" : "English",
      "options" : {
        "language" : "en",
        "region" : "US"
      }
    },
    {
      "name" : "Spanish",
      "options" : {
        "language" : "es",
        "region" : "ES"
      }
    },
    {
      "name" : "Dark Mode",
      "options" : {
        "userInterfaceStyle" : "dark"
      }
    }
  ],
  "testTargets" : [
    {
      "target" : {
        "containerPath" : "container:MyApp.xcodeproj",
        "identifier" : "MyAppUITests",
        "name" : "MyAppUITests"
      }
    }
  ]
}
使用测试计划运行
xcodebuild test \
  -scheme "MyApp" \
  -testPlan "MyTestPlan" \
  -destination "platform=iOS Simulator,name=iPhone 16" \
  -resultBundlePath /tmp/results.xcresult

CI/CD 集成

并行执行测试
xcodebuild test \
  -scheme "MyAppUITests" \
  -destination "platform=iOS Simulator,name=iPhone 16" \
  -parallel-testing-enabled YES \
  -maximum-parallel-test-targets 4 \
  -resultBundlePath /tmp/results.xcresult
重试失败的测试
xcodebuild test \
  -scheme "MyAppUITests" \
  -destination "platform=iOS Simulator,name=iPhone 16" \
  -retry-tests-on-failure \
  -test-iterations 3 \
  -resultBundlePath /tmp/results.xcresult
代码覆盖率
xcodebuild test \
  -scheme "MyAppUITests" \
  -destination "platform=iOS Simulator,name=iPhone 16" \
  -enableCodeCoverage YES \
  -resultBundlePath /tmp/results.xcresult

# Export coverage report
xcrun xcresulttool export coverage \
  --path /tmp/results.xcresult \
  --output-path /tmp/coverage

调试失败的测试

截取屏幕截图
// Manual screenshot capture
let screenshot = app.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = "Before Login"
attachment.lifetime = .keepAlways
add(attachment)
录制视频

在测试计划或方案中启用:

"systemAttachmentLifetime" : "keepAlways",
"userAttachmentLifetime" : "keepAlways"
打印元素层级结构
// Debug: Print all elements
print(app.debugDescription)

// Debug: Print specific container
print(app.tables.firstMatch.debugDescription)

应避免的反模式

1. 硬编码延迟
// BAD
sleep(5)
button.tap()

// GOOD
XCTAssertTrue(button.waitForExistence(timeout: 5))
button.tap()
2. 基于索引的查询
// BAD - Breaks if order changes
app.tables.cells.element(boundBy: 0)

// GOOD - Uses identifier
app.tables.cells["firstItem"]
3. 测试之间共享状态
// BAD - Tests depend on order
func test1_CreateItem() { ... }
func test2_EditItem() { ... }  // Depends on test1

// GOOD - Independent tests
func testCreateItem() {
    // Creates own item
}
func testEditItem() {
    // Creates item, then edits
}
4. 测试实现细节
// BAD - Tests internal structure
XCTAssertEqual(app.tables.cells.count, 10)

// GOOD - Tests user-visible behavior
XCTAssertTrue(app.staticTexts["10 items"].exists)

录制 UI 自动化(Xcode 26+)

摘自 WWDC 2025-344:

  1. 录制 — 在 Xcode 中录制交互(Debug → Record UI Automation)
  2. 重放 — 通过测试计划在不同设备、语言和配置下运行
  3. 查看 — 在测试报告中观看录制的视频
改进录制的代码
// RECORDED (may be fragile)
app.buttons["Login"].tap()

// ENHANCED (stable)
let loginButton = app.buttons["loginButton"]
XCTAssertTrue(loginButton.waitForExistence(timeout: 5))
loginButton.tap()

资源

WWDC:2025-344、2024-10206、2023-10175、2019-413

文档:/xctest/xcuiapplication、/xctest/xcuielement、/xctest/xcuielementquery

技能:axiom-ui-testing、axiom-swift-testing

评论

支持 Markdown