スナップショットテストは、現在の画面状態を前回のテスト実行時に保存された参照画像(スナップショット)と比較する、自動化されたユーザーインターフェース検証方法です。視覚的な不一致はすべて、開発者の確認を必要とする変更として記録されます。要素の存在を確認するUIテストとは異なり、スナップショットテストはピクセルレベルの変化(ずれ、色の逸脱、レイアウトの問題)を検出します。Android Developers, 2024によると、スナップショットテストは従来のUIテストでは見逃される視覚的リグレッションの最大30%を検出し、一貫性のあるインターフェースを維持するための不可欠なツールとなっています。
重要なポイント
スナップショットテストは、テストがUIコンポーネントをレンダリングし、結果の画像を参照として保存し、後続の実行で現在のレンダリングをこの参照と比較する手法です。画像が一致すればテストは成功です。差異が見つかった場合はテストは失敗し、開発者は変更されたピクセルが強調表示された差分画像を受け取ります。この手法はウェブ開発(Jestスナップショット)から借用され、モバイルプラットフォーム向けに適応されました。
スナップショットテストの主な価値は、予期しない視覚的変更の自動検出です。開発者がグローバルテーマの配色を変更し、偶然にも多数の画面に影響を与える可能性があります。ボタンやテキストの存在をチェックするUIテストではこれに気づきません。スナップショットテストは、影響を受けるすべての画面のすべてのピクセル変更をキャプチャし、変更の影響の全体像を提供します。
Mobile DevOps Summit 2023の調査によると、従来のUIテストに加えてスナップショットテストを使用するチームは、リリースにおける視覚的な欠陥の数を40%削減しています。このアプローチは、デザインシステムとコンポーネントベースのアーキテクチャを採用するプロジェクトで特に効果的であり、1つのベースコンポーネントの変更がアプリケーションの数十もの画面に影響を与える可能性があります。
根本的な違いは検証の対象にあります。UIテストはインターフェース要素の存在、状態、動作をチェックします:「ボタンが表示されている」「テキストにエラーメッセージが含まれている」「押すと新しい画面が開く」。スナップショットテストは完全な視覚的外観をチェックします:要素の配置、マージン、色、フォント、影、角丸。スナップショットテストは「画面は期待通りに見えるか?」という質問に答え、UIテストは「画面は期待通りに動作するか?」に答えます。
実行速度も異なります。UIテストはエミュレーターまたは実機で実行され、アプリケーションの完全な読み込みが必要で、シナリオあたり10秒から1分かかります。Paparazziなどのライブラリを使用するスナップショットテストは、エミュレーターを起動せずに仮想環境でコンポーネントをレンダリングし、テスト時間を100~500ミリ秒に短縮します。スナップショットテストの完全なセット(50~100画面)は、同等のUIテストセットの30~60分ではなく、2~5分で実行されます。
ただし、スナップショットテストはUIテストを置き換えるものではありません。最適な戦略は組み合わせです:スナップショットテストは視覚的リグレッション(基本状態での各画面のレンダリング)をカバーし、UIテストは動作的側面(クリックシナリオ、入力検証、ナビゲーション)をカバーします。この組み合わせにより、CI実行時間を最小限に抑えながら、インターフェースの正確性に90%の自信が得られます。
Androidでは、主要なツールはPaparazziとShotです。Cash AppのPaparazziは、Layoutlibのグラビティレイアウトを使用して、エミュレーターなしでJVMテスト環境でコンポーネントをレンダリングします。KarumiのShotは、実機またはエミュレーターでインストルメンテーションスクリーンショットを撮影し、AShotライブラリを介して参照と比較します。解像度とピクセル密度の違いを考慮します。
Paparazziはエミュレーターの起動を必要としません。レンダリングはLayoutlibを介してJVM上で行われ、単体テストに匹敵する速度を提供します。このライブラリはViewシステムとJetpack Composeの両方をサポートしています。Composeの場合は、paparazzi.snapshot { MyComposable() }モディファイアを使用します。参照はsrc/test/snapshotsに保存され、実行ごとに自動的に比較されます。最大差異パーセンテージはmaxPercentDifferenceで設定可能です。
Point-FreeのSnapshotTestingは、UIImageだけでなく、文字列、JSON、Data、Core Dataストア全体の比較もサポートしています。これにより、UIスナップショットだけでなく、JSONレスポンスのシリアライズとデコードの検証にも対応する汎用ツールとなっています。SwiftUIの場合は、.image(on: .iPhone13)モディファイアとともにassertSnapshot拡張を使用します。record: true戦略により、初回実行時に参照が作成されます。
React Nativeでは、jest-image-snapshotと組み合わせたreact-native-testing-libraryが一般的なソリューションです。スナップショットテストのウェブアプローチは、Node.js環境でコンポーネントをレンダリングし、仮想DOMのJSONスナップショットを比較することでモバイル環境に移植されます。このアプローチはネイティブより高速ですが、精度は低くなります。プラットフォーム固有のフォントレンダリングやシステムコンポーネントの特性を考慮しません。Flutterの場合は、goldensツールキットを使用したゴールデンテストが使用されます。
Android(Paparazzi)とiOS(SnapshotTesting)のスナップショットテストを見てみましょう。どちらの例も、アバター、名前、ステータスを持つユーザーカードというコンポーネントの外観を検証します。テストはコンポーネントをレンダリングし、テストデータを使用して結果をリポジトリに保存された参照画像と比較します。
Paparazziは、レンダリングをキャプチャするために@Testアノテーションとsnapshot()メソッドを使用します。参照はsrc/test/snapshotsフォルダに保存され、次回実行時に比較のために自動的に読み込まれます。
class UserCardSnapshotTest {
@get:Rule
val paparazzi = Paparazzi(
theme = "Theme.MyApp",
maxPercentDifference = 0.1
)
@Test
fun userCard_defaultState() {
val card = UserCard(
name = "Alice Johnson",
status = "Online",
avatarUrl = "https://example.com/avatar.png"
)
paparazzi.snapshot(card)
}
@Test
fun userCard_offlineState() {
val card = UserCard(
name = "Bob Smith",
status = "Offline",
avatarUrl = null
)
paparazzi.snapshot(card, name = "user_card_offline")
}
}
SnapshotTestingはassertSnapshot内で.snapshot()モディファイアを使用します。ライブラリは自動的にフォーマットを決定します。UIViewの場合はUIImage、テキストの場合はString、バイナリデータの場合はDataです。
import SnapshotTesting
import XCTest
class UserCardSnapshotTests: XCTestCase {
func testUserCardDefaultState() {
let card = UserCardView(
name: "Alice Johnson",
status: "Online",
avatarURL: URL(string: "https://example.com/avatar.png")
)
let controller = UIHostingController(rootView: card)
assertSnapshot(matching: controller, as: .image(on: .iPhone13))
}
func testUserCardOfflineState() {
let card = UserCardView(
name: "Bob Smith",
status: "Offline",
avatarURL: nil
)
assertSnapshot(matching: card, as: .image(on: .iPhone13))
}
}
典型的なワークフローは4つの段階で構成されます。初回実行(レコードモード):すべてのスナップショットテストがレコードモードで実行され、参照画像が作成されてリポジトリに保存されます。この段階は、テストの初期設定時またはインターフェースの意図的な変更後に行われます。記録後、参照はコードとともにコミットされ、プロジェクトの一部になります。
後続の実行では、テストは比較モードで動作します。各新しいレンダリングが参照と比較されます。差異が見つかった場合、差分画像が生成されます。参照と一致するピクセルは緑色で、異なるピクセルは赤色で強調表示されます。開発者は差分を調査し、決定を下します。変更が予期されるもの(意図的なデザイン変更)であれば、レコードコマンドで参照が更新されます。予期しないものであれば、バグが修正されます。参照の更新は1回のコマンドで実行されます。Paparazziの場合は`./gradlew recordPaparazzi`、SnapshotTestingの場合は`assertSnapshot(record: true)`です。
Spotify Engineering Blog(2022)によると、説明されたワークフローを使用するチームは、差分画像の分析にテストあたり平均2分を費やしています。50のスナップショットテストのセットの場合、完全な参照更新サイクルには15〜20分かかり、50画面にわたる視覚的変更の手動検証よりも大幅に高速です。
スナップショットテストには根本的な制限があります。環境への敏感性:同じコンポーネントでも、OSのバージョン、画面密度、フォント設定が異なると、レンダリング結果が異なる場合があります。あるマシンで作成された参照が、CIサーバーでのレンダリングと異なることがあります。解決策は、固定の環境パラメータを使用することです。Paparazziの場合は特定のLayoutlibバージョン、SnapshotTestingの場合は正確なデバイスモデルを指定します。
アンチパターンその1:巨大なスナップショット — 画面全体をキャプチャするスナップショットテストは、任意のコンポーネントのわずかな変更ごとに失敗します。正しいアプローチは、個々のコンポーネント(ボタン、カード、入力フィールド)を分離してテストすることです。各コンポーネントは独立してテストされ、変更の原因を正確に特定できます。アンチパターンその2:差分の無視 — 差分画像を分析せずに参照を自動更新すると、スナップショットテストの価値がゼロになります。各差分には開発者の意識的な決定が必要です。
Better Engineering Blog(2023)によると、スナップショットテストは、デザインシステムのコンポーネントと主要な画面を基本状態(空、入力済み、エラー、境界)でカバーする場合に最大の価値を発揮します。レンダリングにおけるタイムスタンプの非決定論的な性質のため、スナップショットテストでアニメーションや動的な状態をカバーすることは非効率的です。そのようなシナリオには、ビデオ録画や手動QAチェックが適しています。
よくある質問
いいえ、スナップショットテストは視覚的な外観をチェックし、UIテストはインターフェースの動作をチェックします。最適な戦略は両方のアプローチを組み合わせることです。スナップショットは視覚的リグレッション用、UIテストはシナリオとナビゲーション用です。スナップショットは「見た目は正しいか」に答え、UIテストは「動作は正しいか」に答えます。
参照は、新しいテーマカラー、マージンの変更、要素の追加や削除など、意識的なデザイン変更のたびに更新されます。更新はレコードモードで実行され、その後、変更がデザイナーの期待と一致することを確認するために、コードレビューで差分画像が確認されます。
まず第一に、デザインシステムのコンポーネント(ボタン、カード、入力フィールド、モーダルウィンドウ)です。次に、基本状態での主要な画面です。スナップショットでテストしないでくださいアニメーション、WebView、マップ、動的コンテンツを含む画面 — 非決定論的な動作により、スナップショットは誤った失敗を引き起こします。
レコードモードとテストモードの両方で同じAPI Levelを使用します。Paparazziの場合は、設定で特定のLayoutlibバージョンを指定します。SnapshotTestingの場合は、デバイスモデルを固定します。Android 14で作成された参照は、システムフォントやMaterialテーマの変更により、Android 12でのレンダリングと異なる場合があります。
CIでは、スナップショットテストは検証モード(verify)で実行されます。テストが失敗した場合、CIはビルドアーティファクトに差分画像を表示します。レコードモード(参照の更新)は、開発者がローカルで実行するか、手動トリガー付きの個別のCIタスクで実行されます。参照画像はリポジトリにコミットする必要があります。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。