同じ XCUITest 一式がローカルでは連続して成功するのに、クラウド Mac へ移すとボタンを見つけられない、待機がタイムアウトする、起動画面で止まるといった問題が断続的に発生することがあります。原因は単に「マシンが遅い」からとは限りません。実行先の揺れ、直前のテストケースが残した状態、固定時間による待機、失敗時の証拠不足のほうが一般的です。対策は、まず入力を固定し、次に暗黙の依存関係を取り除き、最後に再実行を検討する順序で進めます。
実行ごとの入力を固定する
Xcode、テストプラン、シミュレータの実行先、ビルドディレクトリをコマンドに明記し、GUI に前回残された選択内容へ依存しないようにします。クラウド Mac はジョブを長期間実行できるうえ、同じ作業ディレクトリが複数のパイプラインから使われる可能性もあります。そのため、実行ごとに独立した DerivedData と結果バンドルを用意する必要があります。
RUN_ID="${CI_RUN_ID:-local-$(date +%s)}"
ARTIFACTS="$PWD/artifacts/$RUN_ID"
DERIVED="$PWD/.derived/$RUN_ID"
mkdir -p "$ARTIFACTS" "$DERIVED"
xcodebuild test \
-workspace Sample.xcworkspace \
-scheme SampleUITests \
-testPlan Smoke \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-derivedDataPath "$DERIVED" \
-resultBundlePath "$ARTIFACTS/Smoke.xcresult" \
-parallel-testing-enabled NO
不安定なテストケースへの対策を始める際は、まず並列テストを無効にし、直列実行の基準を確立します。その基準が安定してから、グループ単位で並列化を有効にします。複数のシミュレータ、テストデータ、バックグラウンドジョブを同時に変化させると、障害の原因がテストケースなのかスケジューリングなのかを切り分けにくくなります。
実行前に最低限の確認を行う
実行前に xcodebuild -version を記録し、xcrun simctl list devices available で対象の実行先が実際に存在することを確認します。OS 条件を省略してデバイス名だけを指定したり、複数の Runner で同じ DerivedData ディレクトリを共有したりしないでください。
固定時間の待機を観測可能な状態に置き換える
Thread.sleep は手軽に見えますが、実際には「画面の準備が完了した」という条件を「数秒が経過した」という条件に置き換えているだけです。ネットワーク応答が速い場合は時間を無駄にし、アニメーションやディスク処理が少し遅い場合は準備完了前に待機が終わってしまいます。
let app = XCUIApplication()
app.launchArguments += ["--ui-testing", "--reset-state"]
app.launch()
let continueButton = app.buttons["onboarding.continue"]
let ready = continueButton.waitForExistence(timeout: 12)
XCTAssertTrue(ready, "Continue button did not appear")
XCTAssertTrue(continueButton.isHittable)
continueButton.tap()
要素の特定には、ローカライズされた文言やビュー階層ではなく、安定した accessibility identifier を使用します。ボタンが存在していても操作できない場合は、単にタイムアウトを延ばすのではなく、オーバーレイ、アニメーション、スクロール位置を確認する必要があります。
タイムアウト値は障害を判定する境界であり、性能の保証ではありません。通常の変動を吸収できる値にしつつ、実際に処理が停止した場合は早期に失敗させ、現場の情報を残せるようにします。
各テストケースを既知の状態から開始する
断続的な失敗は、直前のテストケースに起因することがよくあります。たとえば、ウェルカム画面がすでにスキップされている、テストデータが残っている、ダイアログが一度しか表示されない、バックグラウンドプロセスが終了していない、といった状態です。最も確実な方法は、テスト用の起動引数が指定されたときにアプリ専用のリセット処理を実行し、テストごとに一意のデータを生成することです。
状態の境界ごとに、次のように対処できます。
| 状態の種類 | 推奨する方法 | 推奨しない方法 |
|---|---|---|
| アプリの設定 | 起動引数でテスト専用のリセットを実行する | インストール後は常に空の状態だと仮定する |
| ローカルデータベース | 固定フィクスチャを読み込むかテスト用データベースを再構築する | 直前のテストケースが書き込んだデータに依存する |
| サーバー側データ | 今回の実行に固有の名前空間を使用する | 複数のジョブで固定レコードを共有する |
| システム権限 | スイートの実行前に一括で準備し検証する | 任意のテストケース内でその都度処理する |
| アプリプロセス | テストケースの終了後に明示的に終了する | クラッシュ後に自動復旧すると仮定する |
スイート単位のクリーンアップではアプリを終了できます。アンインストールは、初回インストール時のフローを検証する必要がある場合に限って実行します。シミュレータ全体を頻繁に消去すると所要時間が増えるだけでなく、環境上の問題を「クリーンアップすれば成功する」状態として覆い隠す可能性があります。
xcrun simctl terminate booted "$APP_BUNDLE_ID" 2>/dev/null || true
再実行を分類のための手段にする
自動再実行を合格条件の代わりにしてはいけません。妥当な方針は、初回の失敗後に一度だけ再実行し、attempt-1.xcresult と attempt-2.xcresult をそれぞれ保存することです。両方とも失敗した場合は、まず再現性のある不具合として扱います。初回だけ失敗し、2 回目に成功した場合は不安定なテストケースとして記録し、待機条件、共有状態、リソース競合の調査を続けます。
最終的な終了コードだけを残さない
失敗するたびに、少なくとも次の情報を収集します。
- 独立した結果バンドルとテストログ;
- 失敗した手順のスクリーンショットと UI 階層;
- Xcode のバージョン、対象シミュレータ、起動引数;
- 実行 ID、テストケース名、開始時刻と終了時刻;
- アプリの終了状態と関連するシステムログの抜粋。
並行ジョブによる上書きを防ぐため、結果バンドルのパスには必ず実行 ID を含めます。ログ内のトークン、認証情報、業務データは、アップロード前にマスキングしてください。
単一テストケースの再現から安定性の検証へ進む
まず -only-testing で対象のテストケースを連続実行し、無関係な変数を減らします。次に、そのテストケースが属するテストクラスを実行し、最後にテストスイート全体へ戻します。スイート全体でのみ失敗する場合は、実行順序への依存、共有データ、リソース競合が存在する可能性が高いと考えられます。
xcodebuild test \
-workspace Sample.xcworkspace \
-scheme SampleUITests \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-only-testing:SampleUITests/CheckoutTests/testSubmit \
-parallel-testing-enabled NO
検証では「一度成功した」だけで判断しないでください。複数回連続して実行し、固定された順序への依存がないことを確認するとともに、待機時間の上限が大きすぎるために総所要時間が著しく増えていないかを確認します。並列テストを再開した後は、異なるシャードが同じテストレコードや同じ出力ディレクトリを操作しないことも検証します。
最終的な目的は、失敗率を再試行の背後に隠すことではありません。実行ごとに入力を確定し、待機条件を明確にし、状態を分離し、完全な証拠を残すことです。この 4 点を徹底することで、クラウド Mac 上の XCUITest は「たまに成功するテスト」から、マージやリリースの判断に利用できるエンジニアリング上のシグナルへ変わります。
よくある質問
失敗した XCUITest は自動で再実行してよいですか?
分類目的の再実行は一度だけにします。最初の失敗を消さず、両方の結果バンドルを保存し、再実行時だけ成功するテストは不安定なテストとして修正対象にします。
UI テストで固定時間の sleep を避ける理由は何ですか?
固定時間では画面の準備完了を保証できず、早く完了した場合には待ち時間が無駄になります。要素の存在、操作可能性、期待する文字列などを期限付きで待機します。
次の開発パイプラインにクラウド Macを選ぶ
3種類の固定構成とアジアの4ノードを比較し、実際のワークロードに合ったレンタル期間を選択できます。レンタル1件につき専用物理マシン1台が割り当てられ、仮想マシンではありません。