専用MacでXCUITestによるiOS UIテストを自動化する
XCUITestを自動化するには、ユーザーがログインしているMacで、シミュレータのdestinationと-resultBundlePathを指定してxcodebuild testを実行します。専用のシミュレータはxcrun simctlで作成します。-parallel-testing-enabled YESを付けるとテストがシミュレータのクローンに分散され、-retry-tests-on-failureを付けると不安定なテストが再実行されます。ここにあるコマンドはすべて、Xcode 26.6とiOS 26.5のシミュレータを入れたMacで実行しました。
最初に必要なもの
- Xcodeと、iOSのシミュレータランタイムが1つ以上入ったMac。
- プロジェクト内のUIテストのターゲットと、それをテストする共有スキーム。
- ログイン済みのデスクトップセッション。UIテストはシミュレータのアプリを操作するので、テストを実行するものはそのセッション内にある必要があります。つまりCIエージェントは、LaunchDaemonではなくLaunchAgentとして起動します。Buildkiteのplistのテンプレートにも同じことが書かれています。GUIモードならXcodeのUIテストができますが、ログインが必要です。
1. Xcodeとランタイムを確認する
xcodebuild -version sudo xcodebuild -runFirstLaunch xcrun simctl list runtimes xcrun simctl list devicetypes | grep iPhone
-runFirstLaunchはパッケージをインストールし、ライセンスに同意します。Xcodeをインストールまたはアップグレードするたびに実行してください。
2. CI専用のシミュレータを作成する
専用のデバイスを使えば、CIと人が手で開いたものを分けられます。bootstatus -bはシミュレータを起動し、準備ができるまで待ちます。
UDID=$(xcrun simctl create "CI iPhone 17" "iPhone 17" com.apple.CoreSimulator.SimRuntime.iOS-26-5) xcrun simctl bootstatus "$UDID" -b echo "$UDID"
destinationにはUDIDを使います:-destination "platform=iOS Simulator,id=$UDID"。name=iPhone 17,OS=26.5のように名前でも指定できますが、2つのデバイスが同じ名前を持つことがあります。
3. result bundle付きでUIテストを実行する
rm -rf build/TestResults.xcresult xcodebuild test \ -project MyApp.xcodeproj \ -scheme MyApp \ -destination "platform=iOS Simulator,id=$UDID" \ -derivedDataPath build/DerivedData \ -resultBundlePath build/TestResults.xcresult
result bundleには、すべてのテスト結果、ログ、スクリーンショット、失敗が入っています。コマンドラインで概要を読むには次のようにします。
xcrun xcresulttool get test-results summary --path build/TestResults.xcresult
デバイスごとの成功、失敗、スキップの件数がJSONで出力されます。bundleはditto -c -k --keepParentでzipにし、CIの実行に成果物として添付してください。
4. 一度ビルドして、何度もテストする
ビルドとテストの実行を分けます。そうすれば、再コンパイルせずに一部だけを再実行できます。
xcodebuild build-for-testing -project MyApp.xcodeproj -scheme MyApp \ -destination "platform=iOS Simulator,id=$UDID" -derivedDataPath build/DerivedData xcodebuild test-without-building -project MyApp.xcodeproj -scheme MyApp \ -destination "platform=iOS Simulator,id=$UDID" -derivedDataPath build/DerivedData \ -only-testing:MyAppUITests/LoginTests/testSignIn
-only-testingには、Target、Target/Class、Target/Class/methodを指定できます。-skip-testingは同じ書き方で逆の動作をします。
5. テストを並列で実行する
xcodebuild test -project MyApp.xcodeproj -scheme MyApp \ -destination "platform=iOS Simulator,id=$UDID" \ -parallel-testing-enabled YES \ -parallel-testing-worker-count 2 \ -resultBundlePath build/TestResults.xcresult
Xcodeはシミュレータをクローンし、テストクラスをクローンに分散します。当社のログでは、iPhone 17のClone 1でテストが動いていました。-parallel-testing-enabledはスキームの設定を上書きします。16 GBのMacでは、ワーカー2つから始め、増やす前に計測してください。クローンはそれぞれ、メモリ上の完全なシミュレータです。複数のデバイスタイプで同時にテストするには、destinationを複数並べ、-maximum-concurrent-test-simulator-destinationsを設定します。
6. 不安定なテストをリトライする
xcodebuild test -project MyApp.xcodeproj -scheme MyApp \ -destination "platform=iOS Simulator,id=$UDID" \ -retry-tests-on-failure \ -test-iterations 3 \ -resultBundlePath build/TestResults.xcresult
xcodebuild -helpによると、失敗したテストは反復回数まで再実行されます。-test-iterationsを付けない場合、上限は3回です。一度失敗してから成功するテストで確認しました。実行はTEST SUCCEEDEDで終わりました。概要には、2つのテストに対して3回のテスト実行と報告されました。
つまりリトライすると不安定なテストは成功扱いになり、不安定さが隠れます。毎回result bundleを読み、どのテストにリトライが必要だったかを記録してください。-test-repetition-relaunch-enabled YESを付けると、試行ごとに新しいプロセスで再実行します。不安定なテストを探すには-run-tests-until-failureを使います。これは-retry-tests-on-failureとは併用できません。
7. 止まったテストに制限をかける
-test-timeouts-enabled YES \ -default-test-execution-time-allowance 120 \ -maximum-test-execution-time-allowance 300
これらのフラグをテストのコマンドに追加します。要素をいつまでも待ち続けるUIテストは、許容時間を過ぎると失敗します。CIのタイムアウトまでマシンを占有することはありません。
8. 実行の合間にシミュレータをきれいにする
xcrun simctl shutdown "$UDID" xcrun simctl erase "$UDID" xcrun simctl delete unavailable
eraseはシミュレータのコンテンツと設定をリセットします。delete unavailableは、現在のXcodeがサポートしなくなったデバイスを削除します。Xcodeをアップグレードするたびに実行してください。
再起動しても動き続ける仕組み
作成したシミュレータは再起動後も残ります。戻ってくる必要があるのは、テストを実行するCIエージェントです。LaunchAgentとして動かし、自動ログインをオンにし、各ジョブの最初にbootstatus -bでCI用のシミュレータを起動します。このコマンドは、すでに起動しているデバイスに対しても安全です。
実際に遭遇したエラーと対処法
Unable to find a device matching the provided destination specifier。その名前かOSが存在しません。xcrun simctl list devicesとランタイムの一覧を確認してください。xcodebuild: error: Existing file at -resultBundlePath。毎回の実行の前に、古いbundleを削除してください。Unable to erase contents and settings in current state: Booted。eraseの前にシミュレータをシャットダウンしてください。
専用Macが役立つ理由
UIテストは、最初のタップまでに多くの時間を使います。ビルド、シミュレータの起動、アプリのインストールを待つからです。電源が入ったままのマシンなら、DerivedDataと起動済みのシミュレータを準備しておけます。当社のベンチマークでは、Xcode 26.6でWikipediaのiOSアプリをビルドしました。小さな変更後のジョブは、ウォームなM6で27秒でした。新規のGitHubホスト型macos-26 runnerでは269秒でした。当社のM6はCPUが12コアなので、並列のクローンが2つあってもビルドの余裕が残ります。
ホスト型CIで足りる場合
1日に数件のプルリクエストで小さなスイートを実行するだけなら、分単位課金が合っています。GitHubのmacOSの単価$0.062/分(2026年9月に確認)なら、月額$139のMacは月2,242分ほどで元が取れます。それより少なければ、ホスト型のままにしてください。多数の実機のiPhoneでテストする必要があるなら、デバイスクラウドが必要です。Mac miniで動くのはシミュレータです。並列のスイートに16 GBを超えるメモリが必要でしょうか。当社のM5 Proのティアは48 GBまたは64 GBですが、予約注文制なので、約1週間の待ち時間を見込んでください。
パイプライン全体のどこにUIテストが入るかは、iOS CI/CDパイプラインのガイドで説明しています。ご自身の数字は計算ツールで試せます。
よくある質問
xcodebuildに、失敗したテストをリトライするフラグはありますか?
+
はい。-retry-tests-on-failureは、失敗したテストを-test-iterationsの回数まで、指定がなければ3回まで再実行します。-run-tests-until-failureとは併用できません。
コマンドラインからXCUITestのテストを並列で実行するには?
+
-parallel-testing-enabled YESを付け、必要なら-parallel-testing-worker-countも付けます。Xcodeはシミュレータをクローンし、テストクラスをクローンに分散します。
XCUITestはヘッドレスのMacで動きますか?
+
ユーザーがログインしているセッションが必要です。UIテストはシミュレータのアプリを操作するからです。CIエージェントをLaunchAgentとして動かし、自動ログインをオンにしてください。
Xcodeを開かずにxcodebuildのテスト結果を読むには?
+
-resultBundlePathを指定し、そのbundleに対してxcrun xcresulttool get test-results summary --pathを実行します。件数がJSONで出力されます。