メインコンテンツへスキップ
バージョン: 29.7

スナップショットテスト

非公式ベータ版翻訳

このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →

UIが予期せず変更されないことを確認したい場合、スナップショットテストは非常に有用なツールです。

典型的なスナップショットテストケースでは、UIコンポーネントをレンダリングし、スナップショットを取得してから、テストと一緒に保存されている参照スナップショットファイルと比較します。2つのスナップショットが一致しない場合、テストは失敗します。これは変更が予期せぬものであるか、参照スナップショットをUIコンポーネントの新しいバージョンに更新する必要があることを意味します。

Jestによるスナップショットテスト

Reactコンポーネントのテストにも同様のアプローチが適用できます。アプリ全体をビルドする必要があるグラフィカルUIをレンダリングする代わりに、テストレンダラーを使用してReactツリーのシリアライズ可能な値を迅速に生成できます。Linkコンポーネントテスト例を参照してください:

import renderer from 'react-test-renderer';
import Link from '../Link';

it('renders correctly', () => {
const tree = renderer
.create(<Link page="http://www.facebook.com">Facebook</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});

このテストが初めて実行されると、Jestは次のようなスナップショットファイルを作成します:

exports[`renders correctly 1`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;

スナップショット成果物はコード変更と共にコミットし、コードレビュープロセスの一部としてレビューする必要があります。Jestはpretty-formatを使用して、コードレビュー中にスナップショットを人間が読みやすい形式にします。以降のテスト実行では、Jestはレンダリングされた出力を前回のスナップショットと比較します。一致すればテストは成功し、一致しなければテストランナーがコード内のバグ(この場合は<Link>コンポーネント)を発見したか、実装が変更されてスナップショットの更新が必要です。

メモ

スナップショットはレンダリングするデータに直接スコープされます。この例ではpageプロパティが渡された<Link>コンポーネントです。つまり、他のファイル(例えばApp.js)で<Link>コンポーネントのプロパティが不足していても、テストは<Link>コンポーネントの使用方法を知らず、Link.jsのみにスコープされているためパスします。また、他のスナップショットテストで異なるプロパティを使って同じコンポーネントをレンダリングしても、最初のテストには影響しません。

情報

スナップショットテストの仕組みや開発背景についてはリリースブログ記事をご覧ください。いつスナップショットテストを使用すべきか理解するにはこの記事が参考になります。Jestを使ったスナップショットテストのegghead動画もおすすめです。

スナップショットの更新

バグ導入後にスナップショットテストが失敗した場合は簡単に特定できます。その場合は問題を修正し、スナップショットテストが再びパスするようにしてください。次に、意図的な実装変更が原因でスナップショットテストが失敗するケースについて説明します。

このような状況は、例のLinkコンポーネントが指すアドレスを意図的に変更した場合などに発生します。

// Updated test case with a Link to a different address
it('renders correctly', () => {
const tree = renderer
.create(<Link page="http://www.instagram.com">Instagram</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});

この場合、Jestは次のような出力を表示します:

コンポーネントが別のアドレスを指すように更新したので、このコンポーネントのスナップショットに変更が生じるのは当然です。更新されたコンポーネントのスナップショットがテストケースのスナップショット成果物と一致しなくなったため、スナップショットテストケースは失敗しています。

この問題を解決するには、スナップショット成果物を更新する必要があります。Jestをフラグ付きで実行し、スナップショットを再生成するよう指示できます:

jest --updateSnapshot

上記のコマンドを実行して変更を承認してください。単一文字の -u フラグを使っても同様にスナップショットを再生成できます。これにより、失敗したすべてのスナップショットテストについてスナップショットアーティファクトが再生成されます。意図しないバグによる失敗したスナップショットテストがある場合、バグを含んだ動作のスナップショットを記録しないよう、スナップショット再生成前にバグを修正する必要があります。

スナップショット再生成を特定のテストケースに限定したい場合は、追加で --testNamePattern フラグを渡すことで、パターンに一致するテストのみスナップショットを再記録できます。

スナップショットの例をクローンして、Link コンポーネントを変更しJestを実行することで、この機能を試せます。

対話型スナップショットモード

失敗したスナップショットはウォッチモードで対話的に更新することも可能です:

対話型スナップショットモードに入ると、Jestは失敗したスナップショットを1テストずつ順に表示し、失敗した出力を確認する機会を提供します。

ここから各スナップショットを更新するか、次のスキップするかを選択できます:

終了すると、Jestは要約を表示してからウォッチモードに戻ります:

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

インラインスナップショットの動作は外部スナップショット(.snap ファイル)と同一ですが、スナップショットの値が自動的にソースコード内に書き戻されます。これにより、正しい値が書き込まれたか外部ファイルを確認する必要なく、自動生成スナップショットの利点を得られます。

例:

まず、引数なしで .toMatchInlineSnapshot() を呼び出すテストを記述します:

it('renders correctly', () => {
const tree = renderer
.create(<Link page="https://example.com">Example Site</Link>)
.toJSON();
expect(tree).toMatchInlineSnapshot();
});

次にJestを実行すると、tree が評価され、スナップショットが toMatchInlineSnapshot の引数として書き込まれます:

it('renders correctly', () => {
const tree = renderer
.create(<Link page="https://example.com">Example Site</Link>)
.toJSON();
expect(tree).toMatchInlineSnapshot(`
<a
className="normal"
href="https://example.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Example Site
</a>
`);
});

これだけです!--updateSnapshot--watch モードの u キーでスナップショットを更新できます。

デフォルトではJestがソースコードへのスナップショット書き込みを処理します。ただしプロジェクトで prettier を使用している場合、Jestはこれを検出し(設定も尊重して)、代わりにprettierに処理を委譲します。

プロパティマッチャー

スナップショット対象のオブジェクトに生成されるフィールド(IDや日付など)が含まれることがよくあります。これらを含むオブジェクトをスナップショットしようとすると、毎回テストが失敗します:

it('will fail every time', () => {
const user = {
createdAt: new Date(),
id: Math.floor(Math.random() * 20),
name: 'LeBron James',
};

expect(user).toMatchSnapshot();
});

// Snapshot
exports[`will fail every time 1`] = `
{
"createdAt": 2018-05-19T23:36:09.816Z,
"id": 3,
"name": "LeBron James",
}
`;

このようなケースでは、Jestはプロパティごとに非対称マッチャーを提供できます。これらのマッチャーはスナップショットの書き込み/テスト前にチェックされ、受信値の代わりにスナップショットファイルに保存されます:

it('will check the matchers and pass', () => {
const user = {
createdAt: new Date(),
id: Math.floor(Math.random() * 20),
name: 'LeBron James',
};

expect(user).toMatchSnapshot({
createdAt: expect.any(Date),
id: expect.any(Number),
});
});

// Snapshot
exports[`will check the matchers and pass 1`] = `
{
"createdAt": Any<Date>,
"id": Any<Number>,
"name": "LeBron James",
}
`;

マッチャーではない値は正確にチェックされ、スナップショットに保存されます:

it('will check the values and pass', () => {
const user = {
createdAt: new Date(),
name: 'Bond... James Bond',
};

expect(user).toMatchSnapshot({
createdAt: expect.any(Date),
name: 'Bond... James Bond',
});
});

// Snapshot
exports[`will check the values and pass 1`] = `
{
"createdAt": Any<Date>,
"name": 'Bond... James Bond',
}
`;
ヒント

オブジェクトではなく文字列の場合、スナップショットテスト前に文字列のランダム部分を自身で置換する必要があります。
そのために replace()正規表現を使用できます。

const randomNumber = Math.round(Math.random() * 100);
const stringWithRandomData = `<div id="${randomNumber}">Lorem ipsum</div>`;
const stringWithConstantData = stringWithRandomData.replace(/id="\d+"/, 123);
expect(stringWithConstantData).toMatchSnapshot();

他の方法としては、スナップショットシリアライザーの使用や、ランダム部分を生成するライブラリのモック化があります。

ベストプラクティス

スナップショットは、アプリケーション内の予期しないインターフェース変更を検出するための優れたツールです。APIレスポンス、UI、ログ、エラーメッセージなど、あらゆる種類のインターフェースに適用できます。他のテスト戦略と同様に、効果的に活用するために知っておくべきベストプラクティスと従うべきガイドラインが存在します。

1. スナップショットをコードとして扱う

スナップショットをコミットし、通常のコードレビュープロセスの一部としてレビューしてください。これはスナップショットをプロジェクト内の他のテストやコードと同様に扱うことを意味します。

スナップショットを焦点を絞り、簡潔に保ち、これらのスタイル規約を強制するツールを使用することで、可読性を確保してください。

前述のように、Jestはスナップショットを人間が読みやすい形式にするためにpretty-formatを使用しますが、追加ツールの導入が役立つ場合があります。例えばeslint-plugin-jestno-large-snapshotsオプションや、コンポーネントスナップショット比較機能を持つsnapshot-diffなどがあります。これらは簡潔で焦点を絞ったアサーションのコミットを促進します。

目標は、プルリクエストでスナップショットを簡単にレビューできるようにし、テストスイートが失敗した際に根本原因を調査せずにスナップショットを再生成する習慣に対抗することです。

2. テストは決定論的であるべき

テストは決定論的である必要があります。変更されていないコンポーネントで同じテストを複数回実行する場合、毎回同じ結果が得られるはずです。生成されたスナップショットにプラットフォーム固有のデータやその他の非決定的なデータが含まれないようにするのはあなたの責任です。

例えばDate.now()を使用するClockコンポーネントがある場合、このコンポーネントから生成されるスナップショットはテスト実行のたびに異なります。このような場合、テスト実行時に常に一貫した値を返すようにDate.now()メソッドをモックすることができます:

Date.now = jest.fn(() => 1_482_363_367_071);

これで、スナップショットテストを実行するたびにDate.now()は常に1482363367071を返します。その結果、テスト実行時期に関わらずこのコンポーネントに対して同じスナップショットが生成されます。

3. 説明的なスナップショット名を使用する

常に説明的なテスト名やスナップショット名を使用するように心がけてください。最適な名前はスナップショットの期待される内容を説明するものです。これによりレビュアーがスナップショットを検証しやすくなり、誰でも古くなったスナップショットが更新前の正しい動作かどうかを判断しやすくなります。

例えば、以下を比較してください:

exports[`<UserName /> should handle some test case`] = `null`;

exports[`<UserName /> should handle some other test case`] = `
<div>
Alan Turing
</div>
`;

宛先:

exports[`<UserName /> should render null`] = `null`;

exports[`<UserName /> should render Alan Turing`] = `
<div>
Alan Turing
</div>
`;

次に:

exports[`<UserName /> should render null`] = `
<div>
Alan Turing
</div>
`;

exports[`<UserName /> should render Alan Turing`] = `null`;

後者は出力で期待される内容を正確に説明しているため、問題が発生した際に明確にわかります:

よくある質問

いいえ、Jest 20以降では、CIシステムでJestを実行する際に、明示的に--updateSnapshotを渡さない限り、スナップショットは自動的に書き込まれません。すべてのスナップショットはCIで実行されるコードの一部であることが期待されており、新しいスナップショットは自動的にパスするため、CIシステム上のテスト実行をパスすべきではありません。すべてのスナップショットをコミットし、バージョン管理下に置くことをお勧めします。

スナップショットファイルをコミットすべきですか?

スナップショットファイルをコミットすべきですか?

はい。すべてのスナップショットファイルは、対象モジュールとそのテストと共にコミットすべきです。これらはJestにおける他のアサーション値と同様に、テストの一部と見なされるべきです。実際、スナップショットは特定時点でのソースモジュールの状態を表しています。こうすることでソースモジュールが変更された際、Jestは以前のバージョンからの変更点を認識できます。またコードレビュー中に追加のコンテキストを提供し、レビュアーが変更内容をより深く検討できるようになります。

ReactおよびReact Nativeコンポーネントはスナップショットテストの代表的な適用例ですが、スナップショットはあらゆるシリアライズ可能な値をキャプチャできます。出力が正しいかどうかを検証する目的がある場面では常に活用すべき手法です。Jestリポジトリには、Jest自体の出力やJestのアサーションライブラリの出力、さらにJestコードベース各所からのログメッセージをテストする多数の実例が含まれています。JestリポジトリのCLI出力スナップショット例を参照してください。

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

スナップショットテストとビジュアルリグレッションテストはUIテストの異なる手法であり、目的も異なります。ビジュアルリグレッションテストツールはWebページのスクリーンショットを取得し、画像をピクセル単位で比較します。一方スナップショットテストでは、値がシリアライズされてテキストファイルに保存され、差分アルゴリズムで比較されます。それぞれに異なるトレードオフがあり、スナップショットテストが開発された背景はJestブログで解説しています。

スナップショットテストはユニットテストを代替しますか?

スナップショットテストはJestに組み込まれる20種類以上のアサーションの1つに過ぎません。その目的は既存のユニットテストを置き換えることではなく、付加価値を提供しテストを容易にすることです。特定の機能セット(例:Reactコンポーネント)ではユニットテストが不要になる場合もありますが、両者は併用も可能です。

スナップショットテストのパフォーマンスと生成ファイルのサイズは?

Jestはパフォーマンスを考慮して設計改訂され、スナップショットテストも例外ではありません。スナップショットがテキストファイルに保存されるため、高速かつ信頼性の高いテストが実現します。JestはtoMatchSnapshotマッチャーを呼び出す各テストファイルに対し、新しいファイルを生成します。スナップショットのサイズは非常にコンパクトで、Jestコードベース全体のスナップショットファイルサイズは300KB未満です。

スナップショットファイルのコンフリクトを解決するには?

スナップショットファイルは常に対象モジュールの現在の状態を反映している必要があります。したがって、2つのブランチをマージしてスナップショットファイルにコンフリクトが発生した場合、手動で解決するか、Jestを実行して結果を確認しながらスナップショットファイルを更新します。

テスト駆動開発(TDD)の原則はスナップショットテストに適用可能ですか?

スナップショットファイルを手動で作成することは可能ですが、一般的には現実的ではありません。スナップショットはテスト対象モジュールの出力変更を検知するためのものであり、コード設計の指針を提供するものではないからです。

コードカバレッジはスナップショットテストでも機能しますか?

はい。他のテストと同様に動作します。