axiom-xctest-automation by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-xctest-automation使用 XCUITest 编写可靠、可维护的 UI 测试的全面指南。
可靠的 UI 测试需要三件事:
始终为测试关键元素使用 accessibilityIdentifier。
// SwiftUI
Button("Login") { ... }
.accessibilityIdentifier("loginButton")
TextField("Email", text: $email)
.accessibilityIdentifier("emailTextField")
// UIKit
loginButton.accessibilityIdentifier = "loginButton"
emailTextField.accessibilityIdentifier = "emailTextField"
来自 WWDC 2025-344 "Recording UI Automation":
// 不好 - 脆弱的查询
app.buttons["Login"] // 本地化后会失效
app.tables.cells.element(boundBy: 0).buttons.firstMatch // 过于具体
// 好 - 稳定的查询
app.buttons["loginButton"] // 使用标识符
app.tables.cells.containing(.staticText, identifier: "itemTitle").firstMatch
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// 不好 - 硬编码等待
sleep(5)
XCTAssertTrue(app.buttons["submit"].exists)
// 好 - 基于条件的等待
let submitButton = app.buttons["submit"]
XCTAssertTrue(submitButton.waitForExistence(timeout: 5))
// 等待元素出现
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)
}
// 等待网络响应
func waitForNetworkResponse() {
let loadingIndicator = app.activityIndicators["loadingIndicator"]
// 等待加载开始
_ = loadingIndicator.waitForExistence(timeout: 5)
// 等待加载完成
_ = waitForElementToDisappear(loadingIndicator, timeout: 30)
}
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()
}
}
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"))
}
// 清除并输入
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)
}
}
// 滚动直到元素可见
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()
// 处理系统警告框
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()
}
// 收起键盘
if app.keyboards.count > 0 {
app.toolbars.buttons["Done"].tap()
// 或者点击外部区域
// app.tap()
}
测试计划允许使用不同配置运行相同的测试:
<!-- 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"
}
}
]
}
xcodebuild test \
-scheme "MyApp" \
-testPlan "MyTestPlan" \
-destination "platform=iOS Simulator,name=iPhone 16" \
-resultBundlePath /tmp/results.xcresult
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
# 导出覆盖率报告
xcrun xcresulttool export coverage \
--path /tmp/results.xcresult \
--output-path /tmp/coverage
// 手动截图捕获
let screenshot = app.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = "Before Login"
attachment.lifetime = .keepAlways
add(attachment)
在测试计划或方案中启用:
"systemAttachmentLifetime" : "keepAlways",
"userAttachmentLifetime" : "keepAlways"
// 调试:打印所有元素
print(app.debugDescription)
// 调试:打印特定容器
print(app.tables.firstMatch.debugDescription)
// 不好
sleep(5)
button.tap()
// 好
XCTAssertTrue(button.waitForExistence(timeout: 5))
button.tap()
// 不好 - 顺序改变时会失效
app.tables.cells.element(boundBy: 0)
// 好 - 使用标识符
app.tables.cells["firstItem"]
// 不好 - 测试依赖于顺序
func test1_CreateItem() { ... }
func test2_EditItem() { ... } // 依赖于 test1
// 好 - 独立的测试
func testCreateItem() {
// 创建自己的项目
}
func testEditItem() {
// 创建项目,然后编辑
}
// 不好 - 测试内部结构
XCTAssertEqual(app.tables.cells.count, 10)
// 好 - 测试用户可见的行为
XCTAssertTrue(app.staticTexts["10 items"].exists)
来自 WWDC 2025-344:
// 录制的(可能脆弱)
app.buttons["Login"].tap()
// 增强的(稳定)
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
每周安装量
110
代码仓库
GitHub 星标数
606
首次出现
2026年1月21日
安全审计
安装于
opencode95
codex89
gemini-cli88
claude-code86
cursor83
github-copilot83
Comprehensive guide to writing reliable, maintainable UI tests with XCUITest.
Reliable UI tests require three things :
ALWAYS use accessibilityIdentifier for test-critical elements.
// SwiftUI
Button("Login") { ... }
.accessibilityIdentifier("loginButton")
TextField("Email", text: $email)
.accessibilityIdentifier("emailTextField")
// UIKit
loginButton.accessibilityIdentifier = "loginButton"
emailTextField.accessibilityIdentifier = "emailTextField"
From WWDC 2025-344 "Recording UI Automation":
// 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
// 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("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"))
}
// 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)
}
}
// 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()
}
Test plans allow running the same tests with different configurations:
<!-- 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"
}
}
]
}
xcodebuild test \
-scheme "MyApp" \
-testPlan "MyTestPlan" \
-destination "platform=iOS Simulator,name=iPhone 16" \
-resultBundlePath /tmp/results.xcresult
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)
Enable in test plan or scheme:
"systemAttachmentLifetime" : "keepAlways",
"userAttachmentLifetime" : "keepAlways"
// Debug: Print all elements
print(app.debugDescription)
// Debug: Print specific container
print(app.tables.firstMatch.debugDescription)
// BAD
sleep(5)
button.tap()
// GOOD
XCTAssertTrue(button.waitForExistence(timeout: 5))
button.tap()
// BAD - Breaks if order changes
app.tables.cells.element(boundBy: 0)
// GOOD - Uses identifier
app.tables.cells["firstItem"]
// 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
}
// BAD - Tests internal structure
XCTAssertEqual(app.tables.cells.count, 10)
// GOOD - Tests user-visible behavior
XCTAssertTrue(app.staticTexts["10 items"].exists)
From WWDC 2025-344:
// 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
Docs : /xctest/xcuiapplication, /xctest/xcuielement, /xctest/xcuielementquery
Skills : axiom-ui-testing, axiom-swift-testing
Weekly Installs
110
Repository
GitHub Stars
606
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykFail
Installed on
opencode95
codex89
gemini-cli88
claude-code86
cursor83
github-copilot83
通过 LiteLLM 代理让 Claude Code 对接 GitHub Copilot 运行 | 高级变通方案指南
36,300 周安装