スナップショットテストとは?JestとVitestの実装と更新運用を実装者向けに解説
スナップショットテストは、関数やコンポーネントの出力を文字列へ変換して保存し、次回以降の実行結果と差分を取って意図しない変化を検知する手法です。期待値を1つずつ書く手間が消える一方、失敗したときに更新フラグを押すだけで通ってしまうため、運用を設計しないと不具合をそのまま正として固定します。この記事では、アサーションテストとの守備範囲の違い、Jest 30系とVitest 4.1系のAPIと既定値の差、更新フラグとCIの仕様、非決定値の吸収方法、VRTやアクセシビリティツリー検査との棲み分け、そして採用と見送りの条件を実装者の目線で整理します。
まとめ:更新フラグを押す前に差分を読む運用が成否を分ける
スナップショットテストの本質は、期待値を書く作業をツールへ肩代わりさせる代わりに、正しさの判断をレビューの瞬間へ先送りする点にあります。判断そのものが消えるわけではありません。ここを取り違えると、テストは緑のまま欠陥だけが積み上がります。
最初に決めるべきは適用範囲です。出力が決定的で、変更の頻度が低く、差分を目で追えるサイズに収まる箇所へ絞る。APIレスポンスの構造、設定ファイルの生成結果、変換関数の出力あたりが該当します。逆に、文言や属性が毎スプリント動くUIの深い階層をまるごと保存すると、差分はノイズになり誰も読まなくなる。
次に決めるのが更新の作法です。テストが落ちたとき、-uをスイート全体へ打つ運用は禁じ手にしてください。失敗したテスト名を指定して更新し、生成された差分をコミットに含めてレビューへ回す。この2手順を守れているかどうかが、資産になるか負債になるかの分岐点になります。
逆に、仕様として明示したい振る舞いや、境界値の判定をスナップショットへ委ねると、意図が読めないテストが残ります。後半では見送るべき場面を具体的に示します。
スナップショットテストの定義と期待値を書くテストとの守備範囲の違い
まず輪郭を固めます。スナップショットテストが検査するのは「前回と同じか」であって、「仕様どおりか」ではありません。
出力をシリアライズして保存し次回実行の差分で検知する仕組みと流れ
スナップショットテストでは、検査したい値をテスト実行時に文字列へ変換し、ファイルへ書き出します。初回の実行では比較対象が存在しないため、書き出した内容がそのまま正解として記録されます。2回目以降は、生成した文字列と保存済みの内容を突き合わせ、一致しなければテストが失敗する仕組みです。
Vitestの公式ドキュメントは、この失敗の意味を「変更が意図しないものであるか、参照スナップショットを新しい結果へ更新する必要があるかのどちらか」と説明しています。つまり失敗そのものは欠陥の証明ではなく、人間の判断を要求する合図にあたります。対象はReactコンポーネントに限らず、シリアライズ可能な値であれば何でも扱うことが可能です。
期待値を手書きするアサーションテストとの検知範囲の違いと選び方
通常のアサーションでは、書き手が「この入力ならこの出力になるはずだ」という判断をコードとして残します。読む側は、テストを見れば仕様の意図を復元できる。半面、フィールドが30個あるオブジェクトを1つずつ検証する記述は現実的ではありません。
スナップショットは逆の性質を持ちます。出力の全体を丸ごと押さえるため、書き手が想定していなかったフィールドの増減まで検知できる。ただし保存されるのは結果だけで、なぜその値が正しいのかという根拠は残りません。テストダブルの5分類と使い分けを整理したうえで、境界値や分岐条件はアサーションで書き、構造全体の変化検知はスナップショットへ任せる。この分業が現実的な落としどころです。
スナップショットファイルをコミットしてレビュー対象へ含める前提
Jestでは、生成されたスナップショットはテストファイルと同じ階層の__snapshots__ディレクトリへ、拡張子.snapのファイルとして保存されます。テストファイルがLink.test.jsならLink.test.js.snapという命名です。
公式ドキュメントは「スナップショットの生成物はコード変更と並べてコミットし、コードレビューの一部としてレビューすべきである」と明記しています。この前提が崩れると手法そのものが成立しません。.gitignoreへ入れる、レビューで差分表示を折りたたむ、生成物だからと読み飛ばす。いずれもスナップショットテストを無効化する行為だと理解しておいてください。
Jest 30系とVitest 4.1系のAPIと既定の出力形式に生じる差
APIの構成は両者でほぼ共通ですが、既定値と一部のメソッドに差があります。2026-08-07時点でnpmレジストリを実測した版は、Jestが30.4系、Vitestが4.1系です。
toMatchSnapshotで外部ファイルへ保存するときの基本の書き方
最も基本の形がtoMatchSnapshotです。検査したい値に対して呼び出すと、初回実行で.snapファイルが生成され、以降は自動で比較されます。1つのテストファイルに複数のスナップショットがある場合、テスト名にもとづくキーで区別されるため、テスト名を変更すると古いエントリが取り残される点に注意してください。
投げられた例外を対象にするtoThrowErrorMatchingSnapshotもありますが、エラーメッセージは仕様というより実装の副産物になりやすく、常用は勧めません。
toMatchInlineSnapshotをテストコードへ埋め込むときの使い分け
toMatchInlineSnapshotは、保存先を外部ファイルではなくテストコード内の引数へ書き戻すメソッドです。Jestの公式ドキュメントは、動作は外部ファイル版と同一で、値が「自動的にソースコードへ書き戻される」点だけが異なると説明しています。
使い分けの基準は行数です。数行に収まる小さな出力なら、テストと期待値が同じ画面に並ぶインライン版のほうが読みやすい。数十行を超える構造をインラインへ書き戻すと、テストコードが生成物で埋まり、他のテストが見えなくなります。外部ファイルへ寄せる目安は、レビューでスクロールせずに読める分量です。
toMatchFileSnapshotとprintBasicPrototypeの既定値差
Viteネイティブなテスト基盤であるVitestには、Jestに存在しないtoMatchFileSnapshotがあります。任意の拡張子を持つ独立したファイルと突き合わせるメソッドで、HTMLやCSSの生成結果をそのままの形式で保存したい場合に向きます。
出力形式の既定にも差があります。Vitest公式ドキュメントによれば、オブジェクトのプロトタイプ名を出力するprintBasicPrototypeはVitestで既定false、Jestは29.0.0未満で既定trueです。ほかにヘッダーコメントの記載内容と、カスタムメッセージの区切り文字(Vitestはコロンではなく不等号)が異なります。JestからVitestへ移行する際、ロジックを変えていないのにスナップショットが全件落ちるのはこの差が原因です。移行時は一度まとめて再生成し、その差分だけを独立したコミットに切り分けてください。
更新フラグの扱いとCIで新規スナップショットを通さない既定の仕様
スナップショットテストが失敗したあとの操作は、たった1つのフラグに集約されます。この1つが運用のすべてを決めます。
更新フラグをスイート全体へ打つと不具合をそのまま正へ固定する
更新は--update(短縮形-u)で行い、ウォッチモードならuキーでも実行できます。操作が軽いぶん、落ちたら反射的に打つ習慣が付きやすい。ここが最大の落とし穴です。
スイート全体へ打つと、意図した変更と意図しない退行の区別なく、現在の出力がすべて正解として上書きされます。テストは緑に戻り、欠陥だけが残る。回避策は単純で、テスト名を絞る実行オプションと組み合わせて更新対象を限定し、更新後の差分を必ずgit diffで読んでからコミットする。この2手順をチームの約束事として明文化してください。
CI環境で新規スナップショットが自動生成されない仕様の意味と確認
両ツールとも、CI環境では新規スナップショットの書き込みを既定で無効にしています。Jestの公式ドキュメントは「Jest 20以降、--updateSnapshotを明示的に渡さずにCIで実行した場合、スナップショットは自動的に書き込まれない。新しいスナップショットは自動的に通過してしまうため、CIでのテスト実行を通過させるべきではない」と説明しています。
この仕様が守っているのは、スナップショットファイルのコミット漏れです。手元では生成済みのため緑になり、CIでは生成されないため落ちる。不可解に見えても、生成物がリポジトリへ入っていない事実を機械が検出しているだけです。CIで新規スナップショット由来の失敗が出たら、フラグを足すのではなく、生成物をコミットし忘れていないかを先に疑ってください。
使われなくなったスナップショットを掃除するタイミングの決め方
テストを削除したり名前を変更したりすると、対応するエントリが参照されないまま.snapファイルへ残ります。実行結果には陳腐化した件数として報告され、更新フラグ付きで実行したときに削除されます。
放置しても失敗にはなりませんが、残り続けると2つの害が出ます。ファイルが肥大化して差分が読みにくくなること、そして削除済みのはずの挙動があたかも検査対象であるかのように見えること。テスト名を変更したコミットでは、その場で更新フラグを付けて掃除まで済ませる。C0からC2までの網羅率と計測ツールのように数値で見える指標と違い、陳腐化件数は実行ログを読まないと気づけないため、CIのログ確認項目へ入れておくと取りこぼしが減ります。
非決定値の吸収とスナップショットを小さく保つための設計指針と基準
スナップショットテストが機能しなくなる原因は、ほぼ2つに絞られます。実行のたびに値が変わることと、差分が読めないほど大きくなることです。
プロパティマッチャで日時やIDの揺れを型として残す書き方と判断
生成日時やUUIDを含むオブジェクトをそのまま保存すると、実行するたびに差分が出て毎回失敗します。Jestはこの対策としてプロパティマッチャを用意しており、toMatchSnapshotの引数へ対象フィールドと型を渡すと、値ではなく型として記録されます。日時のフィールドにexpect.any(Date)、IDにexpect.any(Number)を指定すれば、保存内容はAny<Date>やAny<Number>という表記に置き換わる形です。
公式ドキュメントは、テストは決定的であるべきで、日時のような非決定値はモックすべきだとも述べています。どちらを選ぶかは目的次第です。値そのものに意味がない補助的なフィールドはプロパティマッチャで吸収し、日時の計算そのものが検査対象なら固定時刻へモックする。前者を選ぶと、そのフィールドの中身は一切検査されなくなる点を意識して線を引いてください。
肥大化を止めるレビュー基準と静的解析での上限を設ける運用基準
もう1つの敵が肥大化です。ページ全体をレンダリングした結果を保存すると、数百行のHTMLが1エントリになります。この規模になると、差分が10行出ても人間は判断を放棄し、更新フラグを押すだけの作業になる。
Jest公式は、大きすぎるスナップショットを禁止する静的解析ルール(eslint-plugin-jestのno-large-snapshots)の使用を挙げています。行数の上限を機械で縛れば、対象を分割せざるを得なくなる。運用上の目安は次の3点です。
- 1エントリは、レビュー画面で折りたたまずに読める行数へ収める
- ページ全体ではなく、検査したいコンポーネント単位で切り出す
- 上限を超えたら、スナップショットではなくアサーションで書き直す
カスタムシリアライザで出力の表記そのものを安定させる設定方法
値の中身ではなく、文字列化の仕方を制御したい場面もあります。両ツールともexpect.addSnapshotSerializerによる登録と、設定ファイル側での指定に対応しています。
効果が大きいのは、内部実装の都合が出力へ漏れているケースです。フレームワーク由来のラッパーオブジェクトや、内部プロパティが並ぶ構造をそのまま保存すると、ライブラリの更新だけでスナップショットが落ちます。シリアライザで表示対象を絞れば、依存の更新に巻き込まれる頻度が下がる。React Testing Libraryのクエリと使い方で得たDOM要素を対象にする場合も、描画結果のうちどこまでを保存対象にするかを先に決めておくと、後からの調整が減ります。
VRTやアクセシビリティツリー検査との棲み分けと併用時の判断
「スナップショット」という語は、比較対象の異なる複数の手法に使われています。ここを混同すると、導入したのに狙った退行を検知できません。
画像のピクセル比較を行うVRTとの検知対象の違いと使い分けの基準
本記事で扱ってきたのは、シリアライズ済みのテキストを比較する手法です。これに対し、描画結果を画像として保存し、ピクセル単位で比較するのがVRT(Visual Regression Testing)の仕組みと主要ツールにあたります。
検知できる範囲が違います。CSSの変更でレイアウトが崩れても、生成されるDOM構造が同じならテキストのスナップショットは通過する。逆に、見た目に影響しない属性の追加は、画像比較では検知されずテキスト比較では落ちます。画像側はレンダリング環境の差で偽陽性が出やすく、専用の基盤(Node.js環境ならjest-image-snapshotの6.5系など、2026-08-07時点)が必要になる点も判断材料です。
Playwrightのアクセシビリティツリー検査が押さえる構造の範囲
第3の選択肢が、アクセシビリティツリーを対象にする方式です。Playwright(2026-08-07時点で1.62系)のtoMatchAriaSnapshotは、公式ドキュメントの表現では「ページのアクセシビリティツリーのYAML表現」を比較対象にします。役割・属性・テキスト内容の階層構造が記録され、.aria.yml拡張子で外部ファイル化もできます。
この方式は、スタイルの変更に強く、見出し階層やリンクの構造といった意味の側が保たれているかを検査します。3方式の関係は、テキスト比較が実装の出力、画像比較が見た目、ツリー比較が意味の構造、と整理できる。全部入れる必要はなく、退行が起きて困る層はどこかを決めてから1つ選んでください。
APIレスポンスやPython側のテストへ広げるときの適用範囲
フロントエンド専用の手法ではありません。REST APIのレスポンスを対象にすれば、フィールドの増減や型の変化を契約の変更として検知できます。バックエンドのレスポンス整形やDTOの変換処理は、出力が決定的で構造が大きいという条件を満たすため相性が良い部類です。
言語をまたいだ実装もあります。Pythonのpytest向けにはsyrupy(2026-08-07時点でPyPI上5.5系)があり、保存と更新フラグによる再生成という枠組みは同じです。ツールが変わっても勘所は変わらず、更新の判断をレビューへ載せられるかどうかがすべてになります。
スナップショットテストを採用する条件と見送るべき3つの場面の判断
ここは判断を言い切ります。スナップショットテストは記述量を減らす便利機能ではなく、条件が揃った対象に限って効く検知手段です。
出力が決定的で変更が稀な対象へ絞るときの採用条件の置き方と判断
採用してよいのは、次の3条件がすべて成立する場合です。出力がシリアライズ可能で決定的であること、意図的な変更の頻度が低いこと、そして差分がレビューで読めるサイズに収まること。
具体的には、APIレスポンスの構造、設定ファイルやSQLの生成結果、Markdownやテンプレートの変換出力、小さな表示コンポーネントの描画結果が該当します。いずれも「仕様を語る」よりも「勝手に変わっていないことを保証する」目的に向いた対象です。既存のテスト資産がリグレッションテストの範囲選定として整理されているなら、その回帰検知の網へ低コストで面を足す位置づけになります。
差分が読めない規模や頻繁に変わるUIで見送るべき場面の見極め
次の3条件のどれかに当てはまるなら、導入しないでください。
- 出力が数百行規模で、レビューで差分を読み切れない。更新フラグを押す作業に退化する
- 文言やクラス名が毎スプリント変わるUI。正当な変更で毎回落ち、失敗が信号として機能しなくなる
- 境界値や分岐条件など、仕様として意図を明示したい判定。根拠が残らず後任が読めない
特に2つ目は放置すると連鎖します。落ちるのが当たり前になったスイートでは、本物の退行が混ざっても誰も止まりません。テストの検出力そのものを疑うなら、ミューテーションテストによる検出力の計測を併用して、スナップショットが実際に何を守れているかを確かめる手もあります。
保守運用と内製化の現場でスナップショット資産の劣化を止める方法
スナップショットテストが負債化する原因は、書き方よりも運用側の設計にあります。更新の判断基準が明文化されていない、レビューで生成物の差分を読む習慣がない、陳腐化したエントリを誰も掃除しない。いずれもコードレビューとCI運用のルールが定まっていないことの表れです。
既存システムを引き継いだ状態でテスト資産の劣化を止めるには、レビュー観点の整備、CIジョブの設計、社内へのノウハウ移管をまとめて進める必要があります。システム保守運用・内製化支援では、稼働中システムの保守を引き受けながら、テストとCIの運用を社内チームへ移す形での支援も対応しています。自走できる体制になるまでの期間を短縮したい場合の選択肢としてご検討ください。
よくある質問
スナップショットテストの導入検討でよく挙がる論点をまとめます。
スナップショットファイルはバージョン管理へ含めるべきですか?
含めてください。Jest公式ドキュメントは、スナップショットの生成物をコード変更と並べてコミットし、コードレビューの一部としてレビューすべきだと明記しています。リポジトリへ入っていないと、CI環境では新規スナップショットが自動生成されない既定のために失敗し、しかも比較対象が存在しないので退行の検知もできません。生成物ではなく、期待値を記述したテストコードの一部として扱う認識が前提です。
テストが落ちたとき更新フラグを押してよい判断基準は何ですか?
差分を1行ずつ読み、そのすべてが自分の変更の意図と一致していると説明できるときだけです。判断が付かない差分が1つでも混ざっていたら、押す前に原因を特定してください。運用としては、スイート全体ではなくテスト名を絞って更新し、更新後の差分をコミットへ含めてレビューへ回す形を推奨します。全体更新を無条件に許す運用では、この手法は退行検知として機能しません。
スナップショットテストだけでコンポーネントの検査は足りますか?
足りません。Jest公式ドキュメントも、スナップショットテストは単体テストの代替ではなく補完だと位置づけています。検知できるのは前回との差であり、初回に記録された内容が正しいかどうかは検査されないためです。表示条件の分岐、入力に対する振る舞い、エラー時の挙動といった仕様は、期待値を明示したアサーションで別途書いてください。
日時やUUIDを含む出力はどう扱えばよいですか?
2つの方法があります。値そのものに意味がないフィールドは、プロパティマッチャへexpect.any(Date)のような型を渡し、値ではなく型として記録する。日時の計算自体が検査対象なら、システム時刻を固定値へモックして出力を決定的にする。前者を選んだ場合、そのフィールドの中身は検査対象から外れるため、値の正しさを見たい箇所へ適用しないよう注意してください。
JestからVitestへ移行するとスナップショットは作り直しになりますか?
既定の出力形式に差があるため、多くの場合は再生成が必要です。Vitest公式ドキュメントは、ヘッダーコメントの記載、printBasicPrototypeの既定値、カスタムメッセージの区切り文字が異なると説明しています。実務上は、移行のタイミングで一度まとめて更新フラグ付きの再生成を行い、その差分だけを独立したコミットへ切り分ける進め方が安全です。ロジック変更のコミットと混ざると、どちらが原因の差分か追えなくなります。
関連記事
- Vitestとは?Viteネイティブなテスト基盤の仕組みと採用判断を実装目線で解説:スナップショット機能を持つテスト基盤側の選定を扱います。
- VRT(Visual Regression Testing)とは?必要性・メリットと主要ツール・Playwright実装:画像で比較する方式との違いが分かります。
- モックとは?スタブとの違い・テストダブル5分類と単体テストでの使い分け:非決定値を固定するモックの選び方を解説しています。
- Storybookの使い方|インストールからストーリー作成・Next.js連携・テストまで:UI単位で描画結果を切り出す土台になります。
- テストピラミッドとは?単体・結合・E2Eの配分と実装者向けテスト戦略設計:どの層へ組み込むかの前提を整理できます。