axiom-ui-recording について
UI テストを作成する開発者向けに、Xcode 26 の UI Automation 録画機能を解説するガイドです。Simulator での操作記録、生成テストの安定化、テストプランを使った構成別の実行、結果の確認を扱います。
主な機能
- 01アプリ操作を Swift テストとして記録
- 02識別子や待機処理による生成テストの強化
- 03テストプランの構成別にテストを実行
- 04動画、スクリーンショット、操作タイムラインの確認
活用シーン
- 01アプリ操作から UI テストを作成
- 02CI 向けの録画テストを安定化
- 03ローカライズや外観の構成をテスト
- 04UI テスト失敗の調査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-ui-recordingインストール後、Claude Code で/axiom-ui-recordingを入力して読み込まれたか確認します。
詳細
UI オートメーションの記録(Xcode 26 以降)
ユーザーの操作を記録して UI テストを作成する、Xcode 26 の UI オートメーション記録機能について説明します。
3 フェーズのワークフロー
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 │
│ │
└─────────────────────────────────────────────────────────────┘
フェーズ 1:記録
記録を開始する
- Xcode で UI テストファイルを開きます
- テストメソッド内にカーソルを置きます
- Debug → Record UI Automation を選択します(または記録ボタンを使います)
- アプリがシミュレーターで起動します
- 操作を行うと、Xcode がコードを生成します
- 完了したら記録を停止します
記録される内容
- ボタン、セル、コントロールのタップ
- テキストフィールドへのテキスト入力
- スワイプとスクロール
- ジェスチャ(ピンチ、回転)
- ハードウェアボタンの押下(ホーム、音量)
生成されるコードの例
// 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("[email protected]")
// Recorded: Tap password field, type password
app.secureTextFields["Password"].tap()
app.secureTextFields["Password"].typeText("password123")
// Recorded: Tap login button
app.buttons["Login"].tap()
}
記録したコードの強化
重要:記録したコードは脆弱なことがよくあります。安定性を確保するため、必ず強化してください。
1. アクセシビリティ識別子を追加する
記録されたコードはラベルを使用するため、ローカライズすると動作しなくなります。
// RECORDED (fragile - breaks with localization)
app.buttons["Login"].tap()
// ENHANCED (stable - uses identifier)
app.buttons["loginButton"].tap()
アプリのコードに識別子を追加します:
// SwiftUI
Button("Login") { ... }
.accessibilityIdentifier("loginButton")
// UIKit
loginButton.accessibilityIdentifier = "loginButton"
2. waitForExistence を追加する
記録されたコードは、要素がすぐに存在すると想定しています。
// 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()
3. アサーションを追加する
記録されたコードは、検証を行わずにアクションを実行するだけです。
// 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")
4. 短いクエリを使う
記録されたコードのクエリが過度に具体的な場合があります。
// RECORDED (too specific)
app.tables.cells.element(boundBy: 0).buttons["Action"].tap()
// ENHANCED (simpler)
app.buttons["actionButton"].tap()
クエリ選択のガイドライン
WWDC 2025-344 より:
| シナリオ | 問題 | 解決策 |
|---|---|---|
| ローカライズされた文字列 | 「Login」は言語によって変わります | accessibilityIdentifier を使う |
| 深くネストされたビュー | 長いクエリチェーンは簡単に壊れます | 可能な限り短いクエリを使う |
| 動的なコンテンツ | セルの内容が変わります | 識別子または汎用クエリを使う |
| 複数の一致 | クエリが多数の要素を返します | 一意の識別子を追加する |
ベストプラクティス
- ラベルより識別子を優先する
- 動作する範囲で最短のクエリを使う
- インデックスベースのクエリを避ける(
element(boundBy: 0)) - 動的なコンテンツに識別子を追加する
フェーズ 2:テストプランで再生する
テストプランを使うと、同じテストを複数の構成で実行できます。
テストプランを作成する
- File → New → File → Test Plan
- テストターゲットを追加します
- 構成を設定します
テストプランの構造
{
"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
}
構成オプション
| オプション | 目的 |
|---|---|
language | テストのローカライズ |
region | 地域ごとの書式設定のテスト |
userInterfaceStyle | ダーク/ライトモードのテスト |
targetForVariableExpansion | 構成に使用するアプリターゲット |
testTimeoutsEnabled | タイムアウトの適用を有効にする |
defaultTestExecutionTimeAllowance | タイムアウト(秒) |
テストプランで実行する
# Command line
xcodebuild test \
-scheme "MyApp" \
-testPlan "MyTestPlan" \
-destination "platform=iOS Simulator,name=iPhone 16" \
-resultBundlePath /tmp/results.xcresult
# In Xcode
# Product → Test Plan → Select your plan
# Then Cmd+U to run tests
フェーズ 3:レビュー
テストレポートの機能
テスト完了後:
- レポートナビゲーターでテスト結果を表示する
- 各テストの動画記録を確認する
- 失敗箇所のスクリーンショットを確認する
- アクションのタイムラインを分析する
添付ファイルを有効にする
テストプランまたはスキームで設定します。
"options": {
"systemAttachmentLifetime": "keepAlways",
"userAttachmentLifetime": "keepAlways"
}
カスタムスクリーンショットを撮影する
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)
}
よくあるパターン
ログインフローテンプレート
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("[email protected]")
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))
}
ナビゲーションフローテンプレート
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)
}
フォーム検証テンプレート
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()
}
トラブルシューティング
記録が開始されない
- テストメソッド内にいることを確認します
- シミュレーターが利用可能か確認します
- アプリがビルドされ、実行できることを確認します
- Xcode を再起動してみます
記録したコードが動作しない
- 操作の前に waitForExistence を追加します
- アクセシビリティ識別子が設定されているか確認します
- クエリを可能な限り短い形式に簡略化します
- アプリを手動で実行して、フローが動作することを確認します
ローカルではテストが成功するが、CI では失敗する
- CI マシンの動作が遅い場合に備えて、タイムアウトを延長します
- アニメーションに対する明示的な待機を追加します
- シミュレーターの構成が一致しているか確認します
- テストのセットアップでアニメーションを無効にします:
app.launchArguments = ["--disable-animations"]
アンチパターン
CI で記録したコードをそのまま使わない
// BAD - Raw recorded code
app.buttons["Login"].tap()
app.textFields["Email"].typeText("[email protected]")
// GOOD - Enhanced for CI
let loginButton = app.buttons["loginButton"]
XCTAssertTrue(loginButton.waitForExistence(timeout: 10))
loginButton.tap()
座標をハードコードしない
// BAD - Coordinates from recording
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
// GOOD - Use element queries
app.buttons["centerButton"].tap()
アサーションを省略しない
// 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))
リソース
WWDC:2025-344、2024-10206、2019-413
ドキュメント:/xcode/testing/recording-ui-tests、/xctest/xcuiapplication
スキル:axiom-xctest-automation、axiom-ui-testing
コメント
Markdown 対応