同じコミットで、開発者のMacではすべてのテストが実行されるのに、クラウドMacでは一部しか実行されない場合、最もよくある原因はテストコードではありません。多くの場合、.xctestplan の設定が気付かないうちに変わっています。Xcode上で一時的にテストをスキップした、診断項目を無効にした、あるいはローカル環境でしか使えない環境変数をプランファイルに保存した、といった変更です。対策は、XCTestPlanをGUIの付随設定ではなく、ビルド入力として扱うことです。
まずテスト契約の範囲を固定する
再現可能なテスト実行エントリには、少なくとも共有Scheme、XCTestPlan、実行先、コマンドライン引数が必要です。最初に、Schemeとテストプランをコマンドラインから検出できることを確認します。
WORKSPACE="${WORKSPACE:-App.xcworkspace}"
SCHEME="${SCHEME:-App}"
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-showTestPlans
想定したプランが出力されない場合は、SchemeがSharedに設定されているか、プランファイルがリポジトリに追加されているか、SchemeのTestアクションがそのプランを参照しているかを確認します。存在しないプランをCI引数で一時的に補ってはいけません。ローカルの実行エントリとパイプラインの分岐をさらに広げるだけです。
プランでは、次の項目を明示的にレビューします。
| 項目 | 確認すべき内容 | よくある設定差分 |
|---|---|---|
| Test Targets | どのユニットテストとUIテストを実行するか | 新しいターゲットが追加されていない |
| Selected Tests | 指定したテストだけを実行する設定になっていないか | デバッグ用の選択状態がコミットされた |
| Skipped Tests | スキップ項目ごとに担当者と期限が設定されているか | 失敗するテストが恒久的に隠された |
| Configurations | 言語、地域、起動引数 | ローカル設定がデフォルト設定を上書きした |
| Diagnostics | クラッシュ、スレッド、パフォーマンスの診断方針 | 高速化のために誤って無効化された |
| Parallelization | どのターゲットで並列実行を許可するか | 状態を共有するテスト同士が干渉した |
テストのスキップは修正ではありません。新たに追加されたskipped testは、コード変更と同様にレビューし、復帰条件を明記する必要があります。
読みやすい正規化差分を生成する
XCTestPlanは構造化ファイルです。元のテキストをそのままレビューすると、フィールド順序や自動生成された識別子がノイズになります。実行時の意味に影響しない設定識別子だけを除外した正規化ベースラインを、リポジトリに保存できます。
PLAN="App.xctestplan"
CURRENT=".ci/xctestplan.current.json"
BASELINE=".ci/xctestplan.baseline.json"
mkdir -p .ci
plutil -convert json -o - "$PLAN" |
jq -S 'del(.configurations[]?.id)' > "$CURRENT"
diff -u "$BASELINE" "$CURRENT"
初回導入時は、$CURRENT を手動で確認してからベースラインとしてコピーし、コミットします。以降のパイプラインでは現在のファイルだけを生成し、diff を実行します。ターゲット、スキップ項目、環境変数、引数、診断オプションはすべて残す必要があります。「きれいな差分」を得るために、実行に関係するフィールドまで削除してはいけません。
正規化スクリプトもテスト基盤の一部です。スクリプトとプランの変更は、同じマージリクエスト内で提示する必要があります。別々にすると、フィルタールールを一度緩和しただけで、その後の設定差分がすべて見えなくなる可能性があります。
機密値とホスト依存を遮断する
プランファイルは変数名の保存には適していますが、トークン、パスワード、秘密鍵の内容、開発者固有のディレクトリを保存する場所ではありません。まず、有効な環境変数を再帰的に抽出します。
plutil -convert json -o - App.xctestplan |
jq -r '
.. |
objects |
.environmentVariableEntries? // empty |
.[]? |
select(.enabled == true) |
[.key, .value] |
@tsv
'
出力を確認するときは、特に3種類の内容を遮断します。シークレットに見える長い文字列、/Users/誰か/ 形式の絶対パス、対話型Shellでしか利用できないツールパスです。実際の機密値は、CIの管理された環境から実行時に注入します。テストコードは変数名だけを参照し、値がない場合は明確なエラーを返すようにします。
設定の上書き優先順位を制御する
XCTestPlan、Scheme、xcodebuild 引数、テストコードのいずれでも起動引数を設定できます。優先順位は1つに統一することを推奨します。プランには安定したデフォルト値を保存し、CIは機密値と今回の実行識別子だけを注入し、テストコードではプロセスレベルの設定を変更しません。パイプラインで -only-testing または -skip-testing を使う場合は、レビュー可能なスクリプトに引数を記述し、ジョブ設定画面の一時入力欄に隠さないようにします。
実行先を固定して証跡を保存する
まず xcrun simctl list devices available を使い、現在のノードにインストールされているデバイスを選択します。そのUDIDをパイプラインの入力値として渡します。「最新OS」のような曖昧な指定よりも、デバイスとランタイムを固定したほうが差異を説明しやすくなります。
DEVICE_UDID="${DEVICE_UDID:?Set DEVICE_UDID from simctl}"
RESULT_PATH="${RESULT_PATH:-artifacts/CI.xcresult}"
rm -rf "$RESULT_PATH"
mkdir -p "$(dirname "$RESULT_PATH")"
xcodebuild test \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-testPlan CI \
-destination "platform=iOS Simulator,id=$DEVICE_UDID" \
-resultBundlePath "$RESULT_PATH"
成功か失敗かにかかわらず、.xcresult、完全なコマンド、コミット識別子、Xcodeのバージョン、選択したデバイスのUDIDを保存します。ログの末尾数十行だけを残してはいけません。テストが実行されなかった場合、プロセスがクラッシュした場合、アサーションが失敗した場合では、必要な証跡が異なります。結果バンドルには、テスト階層、添付ファイル、診断情報が保存されます。
並列実行は保守的な設定から始めます。同じデータベース、固定ポート、共有ファイルに依存するテストターゲットでは、すぐに並列実行を有効にすべきではありません。まず状態を完全に分離し、その後でターゲットごとに並列実行を許可します。リトライで競合状態を隠してはいけません。
監査をマージゲートに組み込む
最終的なゲートは、決められた順序で実行します。Schemeがプランを検出できることを確認し、正規化ファイルを生成してベースラインと比較し、機密値と絶対パスをスキャンし、スキップ一覧を検証してから、最後にテストを実行します。これにより、シミュレータを起動する前に設定エラーを検出でき、待ち時間と診断時間を削減できます。
マージ前には、次のチェックリストで確認します。
- Schemeが共有され、プランファイルがバージョン管理されている。
- 新しいテストターゲットが正しいプランに追加されている。
- すべてのskipped testsに理由、担当者、復帰条件が設定されている。
- プランに実際の機密値や個人ディレクトリが含まれていない。
- CIにテスト範囲を上書きする隠れた引数がない。
- デバイスUDID、Xcodeのバージョン、結果バンドルがすべて記録されている。
- 正規化ルールが実行時の意味に関わるフィールドを削除していない。
- 失敗した実行でも
.xcresultがアーカイブされる。
XCTestPlanのすべての変更をコードレビューで理解できるようにすれば、「ローカルでは全件成功したのに、クラウドではテストが抜けた」という問題は、偶発的な謎ではなくなります。実行前に遮断できる設定差分として扱えるようになります。
よくある質問
共有Schemeだけでは不十分なのはなぜですか?
Schemeは実行入口を示しますが、対象テスト、設定、引数、環境変数、言語、除外一覧はXCTestPlan側で変えられます。CIでは両方と実行コマンドを固定します。
XCTestPlanにアクセストークンを保存してよいですか?
保存しません。計画には変数名か機密性のない既定値だけを置き、実値は管理されたCI環境から実行時に注入し、ログや添付にも残さないようにします。
正規化によって重要な変更を見落としませんか?
実行結果に影響しない自動生成IDだけを除外します。対象、選択項目、除外項目、診断設定、引数、構成名は残し、正規化規則自体もレビュー対象にします。
開発パイプライン向けに固定クラウドMacを構成
MangoVMでは、M4とM4 Proの2種類のApple Silicon専有物理ノードを提供しています。利用期間、リージョン、ストレージの追加オプションを選択すると、注文内容をすべて確認できます。