単体テスト、APIテスト、UIテストを複数のXcode Test Planに分割したあと、最も起こりやすい問題はテストの失敗ではなく、想定どおりにテストが実行されないことです。ローカルのSchemeでデフォルトプランを切り替える人もいれば、一時的にテストケースをスキップしたままファイルをコミットする人もいます。また、環境変数を変更してもCIパイプラインに反映されないことがあります。クラウドMacはリポジトリ内の設定を忠実に実行します。そのため、最初に行うべきなのは再試行回数を増やすことではなく、テストプラン自体をレビュー対象のコードとして扱うことです。
追跡可能なエントリポイントを1つに固定する
Schemeは共有ファイルにする必要があります。通常のパスはApp.xcodeproj/xcshareddata/xcschemes/App-CI.xcschemeです。ユーザーディレクトリ配下のxcuserdataには依存しないでください。そこにある設定は、安定してバージョン管理へ取り込まれません。
SchemeのTest Actionから、リポジトリ内のApp-CI.xctestplanを参照し、CIパイプラインではプラン名を明示的に渡します。ビルドノードが処理を開始したら、まずプランが認識されていることを確認します。
set -euo pipefail
xcodebuild \
-workspace App.xcworkspace \
-scheme App-CI \
-showTestPlans
xcodebuild \
-workspace App.xcworkspace \
-scheme App-CI \
-testPlan App-CI \
-destination 'platform=iOS Simulator,name=iPhone 16' \
test
-testPlanは省略できません。省略すると、Schemeのデフォルト値が変更されてもコマンド自体は成功し、実際に実行されるテストセットだけが変わっている可能性があります。
プランファイルを検証可能な契約として定義する
.xctestplanはJSONファイルなので、静的検査に適しています。ゲートでは、少なくとも次の4種類のフィールドを検査する必要があります。
| 検査項目 | 必須ルール | 失敗が示す内容 |
|---|---|---|
| configurations | CI設定を含める | CIパイプラインのエントリポイントが削除または改名された |
| testTargets | 合意済みのターゲットを含める | 一部のテストグループが実行されていない |
| skippedTests | 許可リストにある項目だけを認める | 説明のないスキップ項目が追加された |
| environmentVariableEntries | 機密値を禁止し、重要なキーの存在を必須とする | 環境に差異が生じたか、情報がリポジトリに混入した |
JSON全体をテキストの並び順まで直接比較しないでください。Xcodeは配列を並べ替えたり、フィールドを追加したりすることがあります。文字単位の比較では、意味のない失敗が発生します。構造として読み取り、チームが実際に依存している制約だけを検証してください。
テストプランのゲートは、あらゆる変更を阻止するためのものではありません。「何が実行されなくなったのか」「どの環境設定が変わったのか」をマージ前に可視化するためのものです。
スクリプトでターゲットの欠落とスキップ項目を阻止する
次のスクリプトは、CI設定、必須のテストターゲット、未登録のスキップ項目を検査します。許可リストはごく短く保ち、コードレビューでは各項目を削除する条件の説明を必須にしてください。
#!/usr/bin/env python3
import json
import sys
from pathlib import Path
plan = json.loads(Path("App-CI.xctestplan").read_text())
required_targets = {"AppTests", "AppIntegrationTests"}
allowed_skips = {
"AppIntegrationTests/testTemporaryServerResponse"
}
config_names = {item["name"] for item in plan.get("configurations", [])}
if "CI" not in config_names:
sys.exit("Missing CI test configuration")
targets = plan.get("testTargets", [])
target_names = {
item.get("target", {}).get("name")
for item in targets
}
missing = required_targets - target_names
if missing:
sys.exit(f"Missing test targets: {sorted(missing)}")
actual_skips = {
test
for item in targets
for test in item.get("skippedTests", [])
}
unexpected = actual_skips - allowed_skips
if unexpected:
sys.exit(f"Unapproved skipped tests: {sorted(unexpected)}")
このスクリプトはテストコマンドより前に実行します。失敗しても、プランファイルを自動的に書き換えてはいけません。自動修正によって、開発者が意図的に行った設定変更が見えなくなる可能性があるためです。
許可リストに削除条件を設定する
許可リストを恒久的なゴミ箱にしてはいけません。各項目には、少なくとも不具合チケット、担当者、削除条件を関連付けます。別の構造化ファイルを管理したくない場合は、コードレビューテンプレートでこれらの情報を必須にし、現在のリストを出力するスクリプトを定期的に実行できます。
固定の環境変数とCIパイプラインのシークレットを分離する
テストプランには、ノードによって変わらない設定を保存するのが適しています。たとえば、UITEST_MODE=1、固定言語、モックサービスモードなどです。アクセストークン、秘密鍵、使い捨ての認証情報はプランファイルに書き込まず、Schemeの平文引数としても渡さないでください。
クラウドMacのCIパイプラインでは、実行前にシークレットを注入し、テストプロセスから環境変数として読み取れます。一方、監査スクリプトでは、キーが存在することと、値が許可された固定の候補に含まれることだけを検証します。これにより、プランの再現性を維持しながら、機密情報がリポジトリへ混入するのを防げます。
変数展開の対象にも注意が必要です。プランが改名済みのTargetを参照している場合、Xcodeの画面ではファイルを開けても、実行時の変数解決が想定から外れることがあります。プロジェクト名を変更したときは、プランの検査とxcodebuild -listをあわせて実行してください。
失敗の調査に十分な証拠を残す
ゲートを通過したら完全なテストを実行し、結果バンドルを固定ディレクトリへ出力します。
rm -rf artifacts/App-CI.xcresult
xcodebuild \
-workspace App.xcworkspace \
-scheme App-CI \
-testPlan App-CI \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-resultBundlePath artifacts/App-CI.xcresult \
test
失敗時には、現在の.xctestplan、実際に実行したコマンド、.xcresultの3点を保存します。コンソールログの末尾だけでは、テストロジックが失敗したのか、ターゲットが読み込まれなかったのか、プラン設定が変更されたのかを判断できません。
ZoneMiniで固定のテストノードを運用する場合も、長期間稼働している環境が常に同一だと想定せず、ジョブを開始するたびに静的監査を実行してください。共有Schemeがエントリポイントを固定し、テストプランが実行対象を定義し、監査スクリプトが変更を制約し、結果バンドルが証拠を保存します。この4層が揃って初めて、「テスト成功」という結果が一貫した意味を持ちます。
よくある質問
CIのxcodebuildコマンドを固定するだけでは不十分ですか?
コマンドで固定できるのはSchemeとプラン名です。.xctestplan内部の対象、除外項目、環境変数は変更できるため、ファイル内容の監査も必要です。
skippedTestsはすべて禁止すべきですか?
一律禁止は不要です。担当者、理由、解除条件を持つ許可リストに一時的な除外を登録し、未登録の除外追加だけをマージ失敗にします。
開発用MacとクラウドMacで同じテストプランを使う方法は?
Schemeをxcshareddata/xcschemesに置き、.xctestplanとともに管理します。CIでは-testPlanを明示し、実行前に-showTestPlansで認識を確認します。
継続的なビルドタスクに専用クラウドMacを選択
M4、メモリ、ストレージ、ノード、請求期間を確認し、固定ビルド環境を既存のパイプラインに接続します。