ENGINEERING NOTE

クラウドMacでXcodeテストを分割し、ビルド成果物を再利用する

クラウドMacでXcodeテストを分割し、ビルド成果物を再利用する

数千件の単体テストやUIテストを抱えるiOSプロジェクトで xcodebuild test をそのまま実行すると、通常はコンパイル、シミュレータの起動、アプリのインストール、全テストケースの実行までを一度に処理します。最後の数分で失敗しても、再実行時にはコンパイルからやり直しです。クラウドMacでは、この一連の処理を2段階に分ける方が適しています。まずテスト可能なビルドを一度だけ生成し、その成果物を境界の明確な複数のテストシャードで再利用します。

まずビルドと実行を分離する

Xcodeの build-for-testing は、アプリ、テストホスト、テストバンドルをビルドし、実行環境を記述した .xctestrun ファイルを生成します。その後に test-without-building を使用すれば、シャードごとに依存関係の解決やソースコードのコンパイルを繰り返す必要がなくなります。

最初にワークスペース、Scheme、対象デバイス、DerivedDataのパスを固定します。

set -euo pipefail

DERIVED_DATA="$PWD/.ci/DerivedData"
RESULTS="$PWD/.ci/Results"
DESTINATION="platform=iOS Simulator,name=iPhone 16,OS=latest"

rm -rf "$DERIVED_DATA" "$RESULTS"
mkdir -p "$RESULTS"

xcodebuild build-for-testing \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -destination "$DESTINATION" \
  -derivedDataPath "$DERIVED_DATA" \
  CODE_SIGNING_ALLOWED=NO

find "$DERIVED_DATA/Build/Products" -name "*.xctestrun" -print

CODE_SIGNING_ALLOWED=NO を使用できるのは、実機向けの署名に依存しないシミュレータテストだけです。テストターゲットに署名必須のコンポーネントが含まれる場合は、このパラメータを削除し、プロジェクト側の署名設定を有効にしてください。ビルドが成功した後はDerivedDataを移動しないでください。.xctestrun がその中の絶対パスを参照している可能性があります。

シャーディングが解決するのはテストのスケジューリングであり、テスト間の共有状態ではありません。あるテストケースが別のテストケースの先行実行に依存している場合、単一マシンでの直列実行では安定して見えていても、分割後には実際の欠陥が表面化します。

テストリストで安定したシャードを定義する

「1グループあたり約100メソッド」のように手作業で分割しないでください。メソッド名は頻繁に変わるため、保守コストが高くなります。テストTarget、テストクラス、または業務ドメイン単位で分割し、そのリストをリポジトリで管理する方が安定します。

シャード 推奨範囲 含めるのに適したテスト
unit-core 純粋なロジック層 データ変換、検証、ステートマシン
unit-storage 永続化層 データベース、キャッシュ、マイグレーション
ui-account アカウントフロー ログイン、設定、権限画面
ui-checkout 取引フロー 商品選択、確認、例外パス

リストは通常のテキストファイルとして管理できます。

AppTests/ParserTests
AppTests/SessionReducerTests
AppTests/ValidationTests

実行時には、各行を1つの -only-testing: パラメータに変換します。テスト識別子には通常、Target/Class または Target/Class/testMethod の形式を使用します。まず xcodebuild -list を実行してSchemeを確認し、その後、小さな範囲のコマンドで識別子を検証してください。スペルミスによってシャード内のテストが1件も実行されない事態を防げます。

1つのシャードだけを実行する

XCTESTRUN="$(find "$DERIVED_DATA/Build/Products" -name '*.xctestrun' -print -quit)"

xcodebuild test-without-building \
  -xctestrun "$XCTESTRUN" \
  -destination "$DESTINATION" \
  -parallel-testing-enabled NO \
  -only-testing:AppTests/ParserTests \
  -only-testing:AppTests/SessionReducerTests \
  -resultBundlePath "$RESULTS/unit-core.xcresult"

シャードごとに異なる resultBundlePath を使用する必要があります。同じパスを再利用すると証跡が上書きされるだけでなく、並行プロセスが同じディレクトリを取り合う可能性もあります。

並行タスクごとに専用のシミュレータを割り当てる

2つの xcodebuild プロセスが同じシミュレータを同時に使用すると、インストール、起動、権限の状態、クリップボードのデータが互いに干渉する可能性があります。並行実行スロットごとに専用デバイスを用意し、UDIDで実行先を指定してください。

まず、利用可能なランタイムを確認します。

xcrun simctl list runtimes
xcrun simctl list devicetypes

現在の環境で使用するランタイム識別子を取得したら、専用デバイスを作成します。

xcrun simctl create \
  ci-shard-1 \
  com.apple.CoreSimulator.SimDeviceType.iPhone-16 \
  "$RUNTIME_ID"

返されたUDIDを、管理された実行設定に保存します。各実行の前にシャットダウンし、状態を消去してください。

xcrun simctl shutdown "$SIMULATOR_UDID" 2>/dev/null || true
xcrun simctl erase "$SIMULATOR_UDID"
xcrun simctl boot "$SIMULATOR_UDID"
xcrun simctl bootstatus "$SIMULATOR_UDID" -b

続いてdestinationを platform=iOS Simulator,id=$SIMULATOR_UDID に変更します。言語、権限、テストデータの事前設定が必要なタスクでは、erase の後に必ず同じ手順で設定を投入してください。前回の実行で残った状態に依存してはいけません。

並行数はリソース負荷に応じて決める

2つのシャードを明示的に起動したうえで、各シャードでもXcode内蔵の並列実行を有効にすると、並行処理が入れ子になります。シミュレータ、テストプロセス、コンパイル補助プロセスの数が同時に増え、最終的には直列実行より遅くなる可能性があります。最初の構成では -parallel-testing-enabled NO を指定し、シャードスロットは一度に1つずつ増やしてください。

監視時には、少なくとも次の3種類のシグナルを記録します。

シャードは、単にテスト件数を均等にすればよいわけではありません。アプリの再起動を何度も伴うUIテストクラスは、数百件の純粋関数テストより時間がかかる場合があります。直近数回のパイプライン実行時間に基づいてリストを調整できますが、実行時にランダムに割り当ててはいけません。失敗の再現や傾向の比較が難しくなるためです。

失敗したシャードだけを再実行し、証跡を残せるようにする

各シャードでは、終了コード、コンソールログ、個別の .xcresult を保存する必要があります。パイプラインの集約段階が失敗することは許容しても、最初のシャードが失敗した時点で他のシャードの証跡を削除してはいけません。失敗したグループを再実行する際は、元の .xctestrun を引き続き使用し、結果の保存先だけを変更します。

xcodebuild test-without-building \
  -xctestrun "$XCTESTRUN" \
  -destination "platform=iOS Simulator,id=$SIMULATOR_UDID" \
  -parallel-testing-enabled NO \
  -only-testing:AppUITests/CheckoutTests \
  -resultBundlePath "$RESULTS/ui-checkout-retry-1.xcresult"

ソースコード、コンパイルパラメータ、Xcodeのバージョン、またはシミュレータのランタイムが変わった場合は、build-for-testing を再実行する必要があります。古い成果物を新しいコミットの証跡として扱うことはできません。テストリスト、ビルドログ、シャードごとの結果、環境のバージョンを最終的に保存して初めて、失敗の原因がコード、テストの状態、実行環境のどこにあるのかを判断できます。

この構成で重要なのは、テストコマンドを複数コピーすることではなく、3つの所有関係を明確にすることです。ビルド成果物は一度だけ生成し、各シミュレータは単一のシャードが占有し、結果バンドルは決して互いに上書きしません。この3点を徹底すれば、再現性を損なうことなくシャード数を増減し、対象を絞って再実行できるようになります。

よくある質問

Xcode標準の並列テストだけでは不十分ですか?

標準機能は一回の実行内での自動分散に向きます。明示的なシャードは担当範囲、結果ファイル、再実行単位を固定したい場合に有効です。

失敗したシャードだけ再実行できますか?

ビルド成果物とxctestrunが残っており、XcodeやSimulatorの環境が変わっていなければ、test-without-buildingで対象だけ再実行できます。

VMDebug クラウドMac

1回の注文で専用利用できる物理Mac miniが必要ですか?

M4とM4 Proの2構成、4つのレンタル期間、5つの選択可能なノードを確認し、ワークフローに合ったデバイスを選びましょう。

構成を選んで注文する