エンジニアリングガイド

クラウドMacで再現可能なiOSシミュレーターテスト基盤を作る

クラウドMacで再現可能なiOSシミュレーターテスト基盤を作る

同じ UI テストがローカルでは成功するのに、リモートノードではランダムにタイムアウトする場合、原因はアプリケーションコードではなく、シミュレーターに残った状態、シャード境界の変化、または過剰な並列実行にあることが少なくありません。信頼できるテストマトリクスでは、実行のたびに「どのランタイムを使用したか」「どのテストケースを同じグループで実行したか」「失敗時の記録をどこに保存したか」という3点を確認できる必要があります。ここでは xcodebuildsimctl を使い、再現可能な実行フローを構築します。

まずマトリクスの範囲を決める

最初からすべてのデバイスと OS バージョンを網羅しようとしてはいけません。まず主要なランタイムと代表的なデバイスを1つずつ選び、安定したベースラインを確立してから互換性確認用の組み合わせを追加します。実行前には、Xcode のバージョン、macOS のバージョン、ランタイム識別子、コミット番号、テストリストを記録します。デバイス名は一意のキーには適しません。同名のシミュレーターが同時に存在できるため、UDID を保存してください。

項目 ベースラインの選択 拡張する条件
OS ランタイム 現在のプロジェクトがサポートする主要バージョン リリース前に最低サポートバージョンを追加
デバイスタイプ 一般的な画面サイズを1つ レイアウト対応を検証する場合にサイズを追加
テストシャード テストクラス単位で固定グループ化 1グループの実行時間が長すぎる場合に分割
並列数 2シャードから開始 リソース使用量が安定してから段階的に増加

テストマトリクスの目的は組み合わせを最大化することではなく、同じコミットと同じ入力から説明可能な結果を得ることです。

使い捨てシミュレーターでベースラインを作る

まず、ノード上に実際に存在するランタイムとデバイスタイプを照会します。スクリプトにバージョン番号をハードコードしないでください。

xcrun simctl list runtimes
xcrun simctl list devicetypes
xcrun simctl list devices available

出力から対応する識別子を選び、シャードごとに独立したデバイスを作成します。名前は識別しやすくするためだけに使い、以降のコマンドではすべて UDID を指定します。

RUNTIME_ID="com.apple.CoreSimulator.SimRuntime.iOS-XX-X"
DEVICE_TYPE_ID="com.apple.CoreSimulator.SimDeviceType.iPhone-XX"

UDID=$(xcrun simctl create "ui-shard-01" "$DEVICE_TYPE_ID" "$RUNTIME_ID")
xcrun simctl boot "$UDID"
xcrun simctl bootstatus "$UDID" -b

開発者が日常的なデバッグに使うシミュレーターを再利用してはいけません。テストによって、権限、地域設定、キーボード、アプリデータ、バックグラウンドタスクが変更される可能性があります。使い捨てデバイスなら、こうした状態を1回の実行内に限定できます。テストケースがカメラ、通知、位置情報の権限に依存する場合は、テスト準備段階で明示的に設定し、前回の許可状態が残っていることを前提にしないでください。

一度だけビルドし、リストに基づいて分割する

マトリクス内の各シャードでビルドを繰り返すべきではありません。最初に build-for-testing を実行して共通のテスト成果物を生成し、その後、各シャードから test-without-building を呼び出します。

DERIVED_PATH="$PWD/.derived-test"

xcodebuild build-for-testing \
  -workspace App.xcworkspace \
  -scheme AppUITests \
  -destination "generic/platform=iOS Simulator" \
  -derivedDataPath "$DERIVED_PATH"

XCTESTRUN=$(find "$DERIVED_PATH" -name "*.xctestrun" -print -quit)

シャードのテストリストはリポジトリにコミットし、アルファベット順や実行時間に応じてその場で分割することで、実行ごとに境界が変わらないようにします。たとえば、ログインとログアウトを同じグループにまとめ、オフライン処理と同期処理を別のグループにまとめます。実行時には Xcode 側の追加並列化を無効にし、並列数を外側のスケジューラーだけで制御します。

xcodebuild test-without-building \
  -xctestrun "$XCTESTRUN" \
  -destination "platform=iOS Simulator,id=$UDID" \
  -only-testing:"AppUITests/AuthenticationTests" \
  -parallel-testing-enabled NO \
  -resultBundlePath "$PWD/results/shard-01.xcresult"

シャード間の実行時間に大きな差がある場合は、直近の複数回の実行時間の中央値に基づいてリストを調整します。ただし、調整後はバージョンを固定し、変更内容を記録してください。実行のたびにスケジューリングスクリプトで自動的に並べ替えると、失敗結果を前回と直接比較できなくなります。

並列数を制御し、確実に後処理する

シミュレーターは、並列数を増やすほど速くなるとは限りません。シャードを1つ増やすたびに、起動プロセス、グラフィックスサービス、テストプロセス、アプリのインスタンスも増加します。まずは2シャードでメモリプレッシャー、CPU 使用率、起動時間、全体の所要時間を観察してください。スワップの発生、bootstatus のタイムアウト、テストケースの所要時間の大幅な変動が見られたら、すぐに並列数を減らします。

スクリプトでは、成功、失敗、中断のいずれの場合も後処理を実行する必要があります。作成した UDID に対する終了処理を登録し、先にシャットダウンしてから削除すれば、次回の実行で古いデバイスを誤って使うことを防げます。

cleanup() {
  xcrun simctl shutdown "$UDID" >/dev/null 2>&1 || true
  xcrun simctl delete "$UDID" >/dev/null 2>&1 || true
}

trap cleanup EXIT INT TERM

ノード上のシミュレーターをすべて消去するグローバル削除コマンドは使用しないでください。同じノード上で別のジョブが動作している可能性があります。削除対象は、今回の実行で作成して記録した UDID だけに限定します。失敗時の状態を残す必要がある場合は、ログと結果バンドルを先にアーカイブしてから削除してください。

結果バンドルで実際の失敗と一時的な失敗を見分ける

各シャードには独立した .xcresult パスを割り当て、標準出力、開始時刻、終了時刻、終了コード、UDID もあわせて保存する必要があります。同じパスを使い回すと、最も価値の高い初回失敗時の記録が上書きされます。失敗後は該当するシャードだけを1回再実行し、すぐにマトリクス全体を再実行しないでください。

初回に失敗し、再実行で成功する場合は、待機条件、アニメーション、ネットワーク依存、状態競合が不安定である可能性が高いと考えられます。2回とも同じ箇所で失敗する場合は、再現性のあるリグレッションである可能性が高くなります。2回の失敗箇所が異なる場合は、ノードのリソースとテスト間で共有されている状態を優先的に確認してください。

本番投入前には、次の順序で確認できます。

  1. ランタイムとデバイスタイプが、現在のノードで取得した照会結果に含まれている。
  2. 各シャードが独立した UDID と結果ディレクトリを使用している。
  3. ビルド成果物は一度だけ生成され、シャードごとに再ビルドされていない。
  4. テストリストがバージョン管理され、実行前にランダムに並べ替えられていない。
  5. 初回失敗時の結果が再実行によって上書きされない。
  6. 中断後に削除されるのは、今回作成したシミュレーターだけである。

よくある質問

1台のクラウドMacで何台のiOSシミュレーターを同時実行できますか?

まず2分割で開始し、メモリプレッシャー、CPU使用率、起動時間、テスト時間のばらつきを確認して増やします。スワップやタイムアウトが発生する場合は同時実行数を減らします。

テスト分割ごとに別のシミュレーターが必要なのはなぜですか?

アプリデータ、権限、クリップボード、バックグラウンドプロセスを分離し、別のテストが残した状態の影響を防ぐためです。

失敗時はテスト全体を再実行すべきですか?

最初のxcresultを保存し、失敗した分割だけを1回再実行します。再実行で成功した場合は不安定な可能性があるテストとして記録し、両方の結果を比較します。

ArmMacs Cloud Mac

プロジェクト期間に合わせて専用物理ノードを利用

固定チップ、メモリ、ストレージ、利用期間、販売中のノードを選択し、在庫がある場合は提供手続きに進みます。

機種を選んで注文