は、出力をまるごと記録し、次回以降その記録と差分がないかを見る手法です。書くのが圧倒的に楽で、一行で導入できます。前回の の実装としても自然な形です。
同時に、最も形骸化しやすい手法でもあります。この回は、なぜそうなるのかを構造として分解し、それでも使う場合の条件を整理します。全否定はしません。条件付きで有効な道具です。
1. 形骸化は「更新が楽」から始まる
スナップショットの利点は、差分が出たときに記録側をワンコマンドで更新できることです。この利便性が、そのまま形骸化の入口になります。
- 1コードを変更し、テストを流す
- 2スナップショットに差分が出て、赤くなる
- 3変更したのだから差分が出るのは当然、と考える
- 4更新コマンドを実行して緑に戻す
- 5差分の中身は読んでいない
3番目の判断が曲者です。多くの場合は本当に当然なので、この推論は大半のケースで正しい。正しいからこそ習慣化し、混ざり込んだ本物の不具合も一緒に承認されます。ここが との決定的な違いです。単体テストで期待値を書き換えるときは「期待値そのもの」を打ち込むので、何を変えたかを意識せざるを得ません。スナップショットの更新にはその契機がなく、手を動かさずに期待値が書き換わります。
この状態のテストは、実行はされているが何も守っていません。しかも には計上され、テスト本数としても数えられます。健全に見えることが、この問題の最も厄介な点です。
2. 差分が大きいほど読まれない
もうひとつの構造的な問題が、記録の粒度です。画面全体の HTML や API レスポンス全体を記録すると、記録は数百行になります。小さな仕様変更でも差分は広範囲に及びます。
| 記録の粒度 | 差分の読みやすさ | 検知できる範囲 | 形骸化しやすさ |
|---|---|---|---|
| 出力全体(画面・レスポンス丸ごと) | 低い | 広い | 高い |
| 注目する部分だけを抜き出して記録 | 高い | 狭い | 低い |
| 項目を明示して個別に検証 | 最も高い | 定義した範囲 | 最も低い |
検知できる範囲と読みやすさは逆向きに効きます。広く記録すれば検知力は上がりますが、読まれなくなれば検知力はゼロになります。実効的な検知力は「範囲 × 読まれる確率」であって、範囲だけを見ても意味がありません。テストの本数やカバレッジといった指標がこの掛け算を捉えられないことが、スナップショットの問題を見えにくくしています。
3. 使ってよい条件
以上を踏まえると、スナップショットが有効に働く条件がはっきりします。
- 差分が小さく収まる粒度で記録している — 数行から数十行。読める量であること
- 差分がレビューの対象に入っている — 記録ファイルもプルリクエストの差分として人が見る
- 出力が本来安定している対象である — 頻繁に変わる画面には向かない
- 変動要素が除去されている — 時刻やIDが混ざっていれば毎回落ちる(EP.5 参照)
2番目が実務上いちばん効きます。記録ファイルを差分として見せるだけで、無確認の更新はかなり減ります。逆に、記録ファイルをレビュー対象から外していると、ほぼ確実に形骸化します。
記録をテストコードと同じ場所に置き、差分表示から除外しない。これだけで運用が変わります。除外設定に入れてしまうと「変更されたことすら見えない」状態になり、手法として成立しなくなります。
4. 抜き出して記録する
粒度の問題は、記録する前に絞ることで解決できます。出力全体ではなく、壊れてはいけない部分だけを抜き出して記録します。
def summarize_invoice(html): """帳票 HTML から、壊れてはいけない値だけを抜き出す。""" doc = parse(html) return { "total": doc.select_one("[data-field=total]").text, "tax": doc.select_one("[data-field=tax]").text, "line_count": len(doc.select("[data-row]")), "has_seal": doc.select_one("[data-field=seal]") is not None, }
def test_invoice_summary(snapshot): html = render_invoice(order_id=1) # 記録されるのは4項目だけ。差分が出れば意味が読み取れる assert summarize_invoice(html) == snapshotこうすると、記録は4項目になります。差分が出れば 「合計金額が変わった」 と即座に読めます。HTML 全体を記録していた場合、同じ変化は数百行の差分のどこかに埋もれます。
この形にすると、スナップショットである必要すら薄れてきます。4項目なら直接 を書いても手間は変わりません。実際、ここまで絞れたなら明示的に検証するほうが、何を守っているかがコードから読めるぶん優れています。
5. 向いている対象、向いていない対象
とはいえ、スナップショットが素直に有効な領域もあります。出力が構造的で、安定していて、手で書き下すには量が多いものです。
# 記録されるのは構造だけ。差分が出れば「型が変わった」と即座に読めるOrderResponse: type: object required: [order_id, total, status] properties: order_id: { type: string } total: { type: integer } status: { type: string, enum: [pending, paid, cancelled] }スキーマのように構造そのものが仕様である対象は、スナップショットと相性がよい例です。差分が出れば「必須項目が減った」「列挙値が増えた」と直接読めます。逆に、差分を読んでも意味が汲み取れない対象 ── 整形済みの HTML や自然文 ── は向きません。差分が意味を持つかどうかが、実用上の分かれ目です。
| 対象 | 向き不向き | 理由 |
|---|---|---|
| 設定ファイルの生成結果 | 向く | 構造的で安定。全項目を手で書くのは冗長 |
| APIの型・スキーマ定義 | 向く | 変わったら気づきたい。差分が意味を持つ |
| データ変換の出力(少数の代表行) | 向く | 件数を絞れば差分が読める |
| 画面の HTML 全体 | 向かない | 変更頻度が高く差分が大きい |
| エラーメッセージの文言 | 向かない | 変わってよいものを固定してしまう |
| LLM の生成文 | 向かない | 毎回変わる。EP.7 で別の方法を扱う |
6. 判断の基準
この回の結論は、「楽に書ける」を選定理由にしないということです。書く手間が下がった以上、楽さを理由に選ぶ意味はもうありません。何を守りたいかを先に決め、それが読める形で残るかで選ぶ。
そのうえでスナップショットを選ぶなら、粒度を絞り、記録をレビュー対象に含める。この2つが守れないなら、明示的な検証に置き換えたほうが、結果として保守も楽になります。判断に迷ったら、その記録ファイルを最後に人が読んだのはいつかを思い出してみてください。思い出せないなら、そのテストはすでに機能していません。
テストの価値は、書く速さではなく、壊れたときに何を教えてくれるかで決まる。
形骸化は更新の手軽さと差分の大きさから生まれる。有効に使う条件は、粒度を絞ることと記録をレビュー対象に含めること。絞りきれたなら明示的な アサーション のほうが優れている場合が多い。次回からは、そもそも出力が毎回変わる対象 ── LLM の検証に入ります。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。