axiom-xctest-automation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseXCUITest Automation Patterns
XCUITest自动化模式
Comprehensive guide to writing reliable, maintainable UI tests with XCUITest.
关于使用XCUITest编写可靠、可维护UI测试的综合指南。
Core Principle
核心原则
Reliable UI tests require three things:
- Stable element identification (accessibilityIdentifier)
- Condition-based waiting (never hardcoded sleep)
- Clean test isolation (no shared state)
可靠的UI测试需要满足三点:
- 稳定的元素识别(accessibilityIdentifier)
- 基于条件的等待(绝不使用硬编码休眠)
- 清晰的测试隔离(无共享状态)
Element Identification
元素识别
The Accessibility Identifier Pattern
可访问性标识符模式
ALWAYS use accessibilityIdentifier for test-critical elements.
swift
// SwiftUI
Button("Login") { ... }
.accessibilityIdentifier("loginButton")
TextField("Email", text: $email)
.accessibilityIdentifier("emailTextField")
// UIKit
loginButton.accessibilityIdentifier = "loginButton"
emailTextField.accessibilityIdentifier = "emailTextField"对于测试关键元素,务必使用accessibilityIdentifier。
swift
// SwiftUI
Button("Login") { ... }
.accessibilityIdentifier("loginButton")
TextField("Email", text: $email)
.accessibilityIdentifier("emailTextField")
// UIKit
loginButton.accessibilityIdentifier = "loginButton"
emailTextField.accessibilityIdentifier = "emailTextField"Query Selection Guidelines
查询选择指南
From WWDC 2025-344 "Recording UI Automation":
- Localized strings change → Use accessibilityIdentifier instead
- Deeply nested views → Use shortest possible query
- Dynamic content → Use generic query or identifier
swift
// 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来自WWDC 2025-344《录制UI自动化》:
- 本地化字符串会变化 → 改用accessibilityIdentifier
- 深层嵌套视图 → 使用尽可能简短的查询
- 动态内容 → 使用通用查询或标识符
swift
// 不良示例 - 脆弱的查询
app.buttons["Login"] // 本地化后会失效
app.tables.cells.element(boundBy: 0).buttons.firstMatch // 过于具体
// 良好示例 - 稳定的查询
app.buttons["loginButton"] // 使用标识符
app.tables.cells.containing(.staticText, identifier: "itemTitle").firstMatchWaiting Strategies
等待策略
Never Use sleep()
绝不使用sleep()
swift
// BAD - Hardcoded wait
sleep(5)
XCTAssertTrue(app.buttons["submit"].exists)
// GOOD - Condition-based wait
let submitButton = app.buttons["submit"]
XCTAssertTrue(submitButton.waitForExistence(timeout: 5))swift
// 不良示例 - 硬编码等待
sleep(5)
XCTAssertTrue(app.buttons["submit"].exists)
// 良好示例 - 基于条件的等待
let submitButton = app.buttons["submit"]
XCTAssertTrue(submitButton.waitForExistence(timeout: 5))Wait Patterns
等待模式
swift
// 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)
}swift
// 等待元素出现
func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 10) -> Bool {
element.waitForExistence(timeout: timeout)
}
// 等待元素消失
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
}
// 等待元素可点击(可见且启用)
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
}
// 等待文本在任意位置出现
func waitForText(_ text: String, timeout: TimeInterval = 10) -> Bool {
app.staticTexts[text].waitForExistence(timeout: timeout)
}Async Operations
异步操作
swift
// 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)
}swift
// 等待网络响应
func waitForNetworkResponse() {
let loadingIndicator = app.activityIndicators["loadingIndicator"]
// 等待加载开始
_ = loadingIndicator.waitForExistence(timeout: 5)
// 等待加载结束
_ = waitForElementToDisappear(loadingIndicator, timeout: 30)
}Test Structure
测试结构
Setup and Teardown
初始化与清理
swift
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()
}
}swift
class LoginTests: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
// 重置应用状态以保证测试干净
app.launchArguments = ["--uitesting", "--reset-state"]
app.launchEnvironment = ["DISABLE_ANIMATIONS": "1"]
app.launch()
}
override func tearDownWithError() throws {
// 测试失败时捕获截图
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()
}
}Test Method Pattern
测试方法模式
swift
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("user@example.com")
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"))
}swift
func testLoginWithValidCredentials() throws {
// 准备 - 导航到登录页面
let loginButton = app.buttons["showLoginButton"]
XCTAssertTrue(loginButton.waitForExistence(timeout: 5))
loginButton.tap()
// 执行 - 输入凭证并提交
let emailField = app.textFields["emailTextField"]
XCTAssertTrue(emailField.waitForExistence(timeout: 5))
emailField.tap()
emailField.typeText("user@example.com")
let passwordField = app.secureTextFields["passwordTextField"]
passwordField.tap()
passwordField.typeText("password123")
app.buttons["loginSubmitButton"].tap()
// 断言 - 验证登录成功
let welcomeLabel = app.staticTexts["welcomeLabel"]
XCTAssertTrue(welcomeLabel.waitForExistence(timeout: 10))
XCTAssertTrue(welcomeLabel.label.contains("Welcome"))
}Common Interactions
常见交互
Text Input
文本输入
swift
// Clear and type
let textField = app.textFields["emailTextField"]
textField.tap()
textField.clearText() // Custom extension
textField.typeText("new@email.com")
// 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)
}
}swift
// 清空并输入
let textField = app.textFields["emailTextField"]
textField.tap()
textField.clearText() // 自定义扩展
textField.typeText("new@email.com")
// 用于清空文本的扩展
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)
}
}Scrolling
滚动
swift
// 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()swift
// 滚动直到元素可见
func scrollToElement(_ element: XCUIElement, in scrollView: XCUIElement) {
while !element.isHittable {
scrollView.swipeUp()
}
}
// 滚动到指定元素
let targetCell = app.tables.cells["targetItem"]
let table = app.tables.firstMatch
scrollToElement(targetCell, in: table)
targetCell.tap()Alerts and Sheets
弹窗与表单
swift
// 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()
}swift
// 处理系统弹窗
addUIInterruptionMonitor(withDescription: "Permission Alert") { alert in
if alert.buttons["Allow"].exists {
alert.buttons["Allow"].tap()
return true
}
return false
}
app.tap() // 触发监视器
// 处理应用内弹窗
let alert = app.alerts["Error"]
if alert.waitForExistence(timeout: 5) {
alert.buttons["OK"].tap()
}Keyboard Dismissal
收起键盘
swift
// Dismiss keyboard
if app.keyboards.count > 0 {
app.toolbars.buttons["Done"].tap()
// Or tap outside
// app.tap()
}swift
// 收起键盘
if app.keyboards.count > 0 {
app.toolbars.buttons["Done"].tap()
// 或者点击外部
// app.tap()
}Test Plans
测试计划
Multi-Configuration Testing
多配置测试
Test plans allow running the same tests with different configurations:
xml
<!-- TestPlan.xctestplan -->
{
"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"
}
}
]
}测试计划允许使用不同配置运行相同的测试:
xml
<!-- TestPlan.xctestplan -->
{
"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"
}
}
]
}Running with Test Plan
使用测试计划运行
bash
xcodebuild test \
-scheme "MyApp" \
-testPlan "MyTestPlan" \
-destination "platform=iOS Simulator,name=iPhone 16" \
-resultBundlePath /tmp/results.xcresultbash
xcodebuild test \
-scheme "MyApp" \
-testPlan "MyTestPlan" \
-destination "platform=iOS Simulator,name=iPhone 16" \
-resultBundlePath /tmp/results.xcresultCI/CD Integration
CI/CD集成
Parallel Test Execution
并行测试执行
bash
xcodebuild test \
-scheme "MyAppUITests" \
-destination "platform=iOS Simulator,name=iPhone 16" \
-parallel-testing-enabled YES \
-maximum-parallel-test-targets 4 \
-resultBundlePath /tmp/results.xcresultbash
xcodebuild test \
-scheme "MyAppUITests" \
-destination "platform=iOS Simulator,name=iPhone 16" \
-parallel-testing-enabled YES \
-maximum-parallel-test-targets 4 \
-resultBundlePath /tmp/results.xcresultRetry Failed Tests
重试失败测试
bash
xcodebuild test \
-scheme "MyAppUITests" \
-destination "platform=iOS Simulator,name=iPhone 16" \
-retry-tests-on-failure \
-test-iterations 3 \
-resultBundlePath /tmp/results.xcresultbash
xcodebuild test \
-scheme "MyAppUITests" \
-destination "platform=iOS Simulator,name=iPhone 16" \
-retry-tests-on-failure \
-test-iterations 3 \
-resultBundlePath /tmp/results.xcresultCode Coverage
代码覆盖率
bash
xcodebuild test \
-scheme "MyAppUITests" \
-destination "platform=iOS Simulator,name=iPhone 16" \
-enableCodeCoverage YES \
-resultBundlePath /tmp/results.xcresultbash
xcodebuild test \
-scheme "MyAppUITests" \
-destination "platform=iOS Simulator,name=iPhone 16" \
-enableCodeCoverage YES \
-resultBundlePath /tmp/results.xcresultExport coverage report
导出覆盖率报告
xcrun xcresulttool export coverage
--path /tmp/results.xcresult
--output-path /tmp/coverage
--path /tmp/results.xcresult
--output-path /tmp/coverage
undefinedxcrun xcresulttool export coverage
--path /tmp/results.xcresult
--output-path /tmp/coverage
--path /tmp/results.xcresult
--output-path /tmp/coverage
undefinedDebugging Failed Tests
调试失败测试
Capture Screenshots
捕获截图
swift
// Manual screenshot capture
let screenshot = app.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = "Before Login"
attachment.lifetime = .keepAlways
add(attachment)swift
// 手动捕获截图
let screenshot = app.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = "Before Login"
attachment.lifetime = .keepAlways
add(attachment)Capture Videos
捕获视频
Enable in test plan or scheme:
xml
"systemAttachmentLifetime" : "keepAlways",
"userAttachmentLifetime" : "keepAlways"在测试计划或scheme中启用:
xml
"systemAttachmentLifetime" : "keepAlways",
"userAttachmentLifetime" : "keepAlways"Print Element Hierarchy
打印元素层级
swift
// Debug: Print all elements
print(app.debugDescription)
// Debug: Print specific container
print(app.tables.firstMatch.debugDescription)swift
// 调试: 打印所有元素
print(app.debugDescription)
// 调试: 打印指定容器
print(app.tables.firstMatch.debugDescription)Anti-Patterns to Avoid
需要避免的反模式
1. Hardcoded Delays
1. 硬编码延迟
swift
// BAD
sleep(5)
button.tap()
// GOOD
XCTAssertTrue(button.waitForExistence(timeout: 5))
button.tap()swift
// 不良示例
sleep(5)
button.tap()
// 良好示例
XCTAssertTrue(button.waitForExistence(timeout: 5))
button.tap()2. Index-Based Queries
2. 基于索引的查询
swift
// BAD - Breaks if order changes
app.tables.cells.element(boundBy: 0)
// GOOD - Uses identifier
app.tables.cells["firstItem"]swift
// 不良示例 - 元素顺序变化后会失效
app.tables.cells.element(boundBy: 0)
// 良好示例 - 使用标识符
app.tables.cells["firstItem"]3. Shared State Between Tests
3. 测试间共享状态
swift
// 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
}swift
// 不良示例 - 测试依赖执行顺序
func test1_CreateItem() { ... }
func test2_EditItem() { ... } // 依赖test1的执行结果
// 良好示例 - 独立测试
func testCreateItem() {
// 创建独立的测试项
}
func testEditItem() {
// 先创建测试项,再执行编辑操作
}4. Testing Implementation Details
4. 测试实现细节
swift
// BAD - Tests internal structure
XCTAssertEqual(app.tables.cells.count, 10)
// GOOD - Tests user-visible behavior
XCTAssertTrue(app.staticTexts["10 items"].exists)swift
// 不良示例 - 测试内部结构
XCTAssertEqual(app.tables.cells.count, 10)
// 良好示例 - 测试用户可见的行为
XCTAssertTrue(app.staticTexts["10 items"].exists)Recording UI Automation (Xcode 26+)
录制UI自动化(Xcode 26+)
From WWDC 2025-344:
- Record — Record interactions in Xcode (Debug → Record UI Automation)
- Replay — Run across devices/languages/configurations via test plans
- Review — Watch video recordings in test report
来自WWDC 2025-344:
- 录制 — 在Xcode中录制交互操作(调试 → 录制UI自动化)
- 重放 — 通过测试计划在不同设备/语言/配置下运行
- 回顾 — 在测试报告中查看视频录制内容
Enhancing Recorded Code
优化录制的代码
swift
// RECORDED (may be fragile)
app.buttons["Login"].tap()
// ENHANCED (stable)
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()Resources
参考资源
WWDC: 2025-344, 2024-10206, 2023-10175, 2019-413
Docs: /xctest/xcuiapplication, /xctest/xcuielement, /xctest/xcuielementquery
Skills: axiom-ui-testing, axiom-swift-testing
WWDC: 2025-344, 2024-10206, 2023-10175, 2019-413
文档: /xctest/xcuiapplication, /xctest/xcuielement, /xctest/xcuielementquery
技能: axiom-ui-testing, axiom-swift-testing