スナップショットテスト

コンポーネントの出力を保存し、変更時に差分を検出するテスト手法

テストフロントエンド

スナップショットテストとは

スナップショットテスト (Snapshot Test) は、コンポーネントやデータの出力を一度ファイルへ保存し、次回以降の実行では保存済みの内容と突き合わせて差分を検出する手法である。機序は 3 段で、①検証したい値を直列化 (シリアライズ) して文字列にする ②初回実行時にその文字列を .snap ファイルへ書き出す ③2 回目以降は生成した文字列と保存済み文字列を比較し、不一致なら差分を出して失敗させる。期待値を人が書かないため、「何が正しいか」を宣言する代わりに「前回と変わっていないか」だけを保証するテストになる。

この性質から、テストの合否は保存済みファイルの中身に完全に依存する。.snap ファイルはコードと同じコミットに含めてレビュー対象にすることが前提であり、差分を読まずに更新すると検知能力がそのまま失われる。

Vitest でのスナップショットテスト

Vitest では toMatchSnapshot() を呼ぶだけでよい。直列化は @vitest/pretty-format が担い、DOM ノードなら要素と属性、プレーンオブジェクトならキーと値を整形した文字列に変換する。

import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import { UserCard } from './UserCard';

describe('UserCard', () => {
  it('renders correctly', () => {
    const { container } = render(
      <UserCard name="Alice" email="alice@example.com" />
    );
    expect(container).toMatchSnapshot();
  });
});

初回実行時に、テストファイルと同じ階層の __snapshots__ ディレクトリへ .snap ファイルが生成される。

// __snapshots__/UserCard.test.tsx.snap
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`UserCard > renders correctly 1`] = `
<div>
  <div
    class="card"
  >
    <h2>
      Alice
    </h2>
    <p>
      alice@example.com
    </p>
  </div>
</div>
`;

キーは describe のブロック名とテスト名を > でつないだものに連番が付く形になる。属性が 1 つでも要素ごとに改行される整形は直列化側の仕様で、そのおかげで属性 1 個の追加が 1 行の差分として現れる (Vitest 4.1 系で確認)。

インラインスナップショット

引数を空にして toMatchInlineSnapshot() と書いて実行すると、テストランナーがテストファイル自体を書き換えて期待値を埋め込む。次の例の引数は、実行後に自動で書き込まれた状態である。

it('formats price correctly', () => {
  expect(formatPrice(1234)).toMatchInlineSnapshot(`"¥1,234"`);
});

別ファイルを開かずにテストコードだけで期待値が読めるため、数行で収まる出力ではこちらが読みやすい。逆に数十行の出力を埋め込むとテストコードが期待値に埋もれるので、その場合は .snap ファイル側に置く。

スナップショットの更新

意図的に出力を変えたときは、保存済みスナップショットを新しい出力で上書きする。

# 意図的な変更後にスナップショットを更新 (短縮形は -u)
npx vitest --update

注意点が 2 つある。1 つは、この更新が差分の中身を問わず一括で行われることで、意図した変更と混入したバグの区別はコマンドではなく人のレビューにしか委ねられていない。もう 1 つは、環境変数 CI が設定されている実行では新しいスナップショットが書き込まれず、保存済みスナップショットの欠落もテスト失敗として扱われる点である。手元で生成した .snap ファイルをコミットし忘れると、CI 側で初めて失敗として現れる。

メリットとデメリット

利点と弱点は同じ性質から生じる。期待値を書かなくて済む手軽さが、そのまま「中身を理解しないまま通る」危険と裏表になっている。

メリットデメリット
意図しない変更を検知大きなスナップショットはレビューが困難
テストの記述が簡単「とりあえず更新」で形骸化しやすい
リグレッション防止実装の詳細に依存

効果的な使い方

向く対象の判断基準は「出力が小さく、決定的で、変わったら必ず知りたいか」の 3 点である。

適する対象適さない対象
小さなコンポーネントの出力大きなページ全体
シリアライズ結果 (JSON)頻繁に変わる UI
エラーメッセージ動的なデータ (日時、ID)

ビジュアルリグレッションテストとの違い

名前が似ているビジュアルリグレッションテストとは、比較する対象そのものが違う。

観点スナップショットテストビジュアルリグレッション
比較対象HTML / テキストスクリーンショット (画像)
CSS の変更検知クラス名が変わらなければ差分に出ない描画結果の画素を比べるため、見た目の変化を捉える
ツールVitest, JestChromatic, Percy
速度高速低速

スナップショットテストは HTML 構造の変更を検知するが、CSS の変更 (色、レイアウト) は検知しない。CSS の変更も検知するにはビジュアルリグレッションテスト (Chromatic) を使う。

現場での応用を知るには関連書籍も役立つ。

この記事は役に立ちましたか?

関連用語

関連する記事