同じ iOS UI コードでも、開発端末では正常に見えていたのに、マージ後にはボタンの欠け、Dynamic Type によるレイアウトの圧迫、ダークモードでの色の不一致といった問題が発生することがあります。視覚回帰ゲートの目的は単体テストを置き換えることではありません。固定した描画条件のもとで、「今回のコミットによって、ユーザーが実際に目にするピクセルが変化したか」という具体的な問いに答えるためのものです。
クラウド Mac はこの種のタスクを継続的に実行するのに適しています。ただし、Simulator の状態、テストデータ、比較ルールをすべてバージョン管理に含める必要があります。「正しく見える」スクリーンショットを数枚保存するだけでは、安定したゲートにはなりません。
安定したスクリーンショットマトリクスを先に定義する
最初から「すべての画面を網羅する」ことを目指してはいけません。まずはログイン前のホーム画面、主要な一覧、詳細画面、空状態、エラー状態など、価値の高い画面を選びます。そのうえで、各画面について次の条件を固定します。
| 条件 | 推奨する固定値 | 変更時の対応 |
|---|---|---|
| デバイス | 明確に指定した 1 つの Simulator モデル | 独立した基準ディレクトリを新規作成 |
| システム | 利用するランタイムを指定 | 基準画像を再レビュー |
| 外観 | ライトとダークを個別に実行 | 異なる外観同士は比較しない |
| 言語 | 対象言語ごとに個別に撮影 | ファイル名に言語コードを含める |
| 文字サイズ | デフォルトサイズから開始 | 大きな文字サイズは別のテストグループにする |
| データ | ローカルフィクスチャまたはテスト API | ランダムな内容への依存を禁止 |
基準画像のパスには Snapshots/<runtime>/<device>/<locale>/<appearance>/ のような構成を使用できます。ディレクトリ階層はやや深くなりますが、失敗時にどの環境で比較されたかをすぐ確認でき、異なるシステム間の描画差を製品の回帰と誤認するのを防げます。
基準画像は永久に正しい答えではありません。レビュー済みの UI 契約であり、更新する場合は必ずコード変更とあわせてレビューする必要があります。
Simulator を固定し、既存状態を繰り返し使わない
手動操作を重ねた 1 台の Simulator を長期間使い回すと、権限ダイアログ、キーボード設定、キャッシュ、テストアカウントなどが残ります。より確実なのは、視覚テスト専用のデバイスを割り当て、実行のたびにシャットダウン、消去、再起動する方法です。
#!/usr/bin/env bash
set -euo pipefail
: "${DEVICE_UDID:?DEVICE_UDID is required}"
xcrun simctl shutdown "$DEVICE_UDID" 2>/dev/null || true
xcrun simctl erase "$DEVICE_UDID"
xcrun simctl boot "$DEVICE_UDID"
xcrun simctl bootstatus "$DEVICE_UDID" -b
xcrun simctl status_bar "$DEVICE_UDID" override \
--time 09:41 \
--batteryState charged \
--batteryLevel 100 \
--wifiBars 3 \
--cellularBars 4
rm -rf TestResults/Visual.xcresult
xcodebuild test \
-workspace Example.xcworkspace \
-scheme ExampleVisualTests \
-destination "id=$DEVICE_UDID" \
-resultBundlePath TestResults/Visual.xcresult
rm -rf TestResults/Attachments
xcrun xcresulttool export attachments \
--path TestResults/Visual.xcresult \
--output-path TestResults/Attachments
現在起動中の booted デバイスに依存してはいけません。並列タスクによって複数の Simulator が同時に起動する可能性があります。UDID を明示的に渡すことで、ステータスバーの上書き、インストール、テストが確実に同じデバイス上で実行されます。
観測可能な状態を使ってスクリーンショットを取得する
視覚テストで特に多い誤りは、アプリを起動してから固定で 2 秒待つことです。マシンの負荷やネットワーク状況によって、2 秒では長すぎる場合もあれば、足りない場合もあります。XCUITest では、「画面が比較可能な状態になった」ことを示すアクセシビリティ要素を待機するべきです。
func capture(_ name: String, readyIdentifier: String) {
let app = XCUIApplication()
app.launchArguments = ["-VisualTestMode", "1"]
app.launch()
let ready = app.descendants(matching: .any)[readyIdentifier]
XCTAssertTrue(ready.waitForExistence(timeout: 15))
let image = XCUIScreen.main.screenshot()
let attachment = XCTAttachment(screenshot: image)
attachment.name = name
attachment.lifetime = .keepAlways
add(attachment)
}
テストモードではカルーセル、スケルトンアニメーション、点滅するカーソルを無効にし、固定の日付、ユーザー名、一覧データを注入します。固定するのは入力であり、テスト対象の UI を別実装に置き換えるわけではありません。画面がリクエスト完了に依存する場合は、スリープ時間をさらに増やすのではなく、読み込み完了を示す安定した識別子を UI に公開します。
フォントとアニメーションを制御する
フォントはシステム提供のもの、またはアプリに同梱したものを使用し、手動で一度だけインストールしたフォントには依存しないようにします。アニメーションは起動引数で無効化でき、撮影前にはスクロールが停止していることも確認する必要があります。常に変化する動画、地図、タイマーには、決定論的なテストフィクスチャを優先して使用します。マスクを使うのは、制御できないごく小さな領域に限ります。
説明可能なピクセルしきい値を設定する
PNG ファイルのバイト列が異なっていても、見た目まで異なるとは限りません。cmp で直接判定すると、エンコード情報の差に影響されやすくなります。比較ツールでは、まず両方の画像を同じサイズと sRGB ピクセル形式にデコードし、その後に各ピクセルのチャンネル差を計算する必要があります。
次の 2 条件を併用することを推奨します。
- チャンネルごとの最大許容差。軽微なアンチエイリアスの変化などを許容します。
- 許容差を超えたピクセルの割合。画像全体のごく小さな割合を超えないようにします。
平均差だけでは、局所的で重大な問題が見過ごされます。たとえば、ボタンが 1 つ消えても、画像全体に占める面積はわずかかもしれません。比較ツールは差分を強調した画像も出力し、しきい値を超えたピクセル数、バウンディングボックス、使用したしきい値を記録する必要があります。重要な確認画面には厳格なルールを適用し、影や複雑なグラデーションを含む画面には個別の設定を用意できます。ただし、テストを「緑」にするためだけにグローバルなしきい値を広げ続けてはいけません。
失敗を再確認可能な証拠として保存する
失敗するたびに、少なくとも基準画像、実際の画像、差分画像、.xcresult を保存します。あわせて、コミット識別子、Xcode のバージョン、システムランタイム、デバイスモデル、言語、外観も記録します。ファイル名はテストスイート、画面状態、環境から構成し、並列タスクによる相互上書きを防ぎます。
調査時は、まず画像サイズとランタイムが変わっていないかを確認し、次にテストデータが安定しているかを調べ、その後に差分のバウンディングボックスを確認します。差分が画面全体に広がっている場合は、フォント、外観、色空間のずれが原因であることが一般的です。差分が 1 つのコントロールに集中している場合に限り、レイアウト変更の可能性が高くなります。
基準画像の更新も独立した手順で行う必要があります。候補画像を生成し、人が差分を確認し、変更意図を確認してから置き換えます。テストタスクが失敗時に基準画像を自動上書きしてはいけません。そうすると、本物の回帰が次回の実行でそのまま受け入れられてしまいます。
完成したゲートには、環境を再構築できること、しきい値を説明できること、失敗を再現できること、という 3 つの特徴が必要です。この 3 点を満たして初めて、スクリーンショットテストは、ときどき人手で整理しなければならない画像置き場ではなく、エンジニアリング上の検査になります。
よくある質問
すべての画素が完全一致した場合だけ合格にすべきですか?
通常は推奨しません。色チャンネルごとの差と、しきい値を超えた画素の割合を併用し、カーソルやアニメーションなど避けられない領域だけを小さく除外します。
同じコミットでも Simulator ごとに画像が変わるのはなぜですか?
端末サイズ、システムのバージョン、フォント、言語、タイムゾーン、表示倍率、ステータスバーが描画に影響します。基準画像は環境の組み合わせごとに管理します。
失敗時に保存すべき成果物は何ですか?
基準画像、実画像、差分画像、テスト結果バンドル、コミット識別子、Simulator の機種とシステムバージョンをまとめて保存します。
検証済みのワークフローを常時稼働するクラウドMacへ
用途に合わせてBookaMac M4またはBookaMac M4 Proを選び、注文時にリージョン、契約期間、ストレージの追加オプションをご確認ください。