スナップショットテスト
スナップショットテストは、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="https://#">Facebook</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});
このテストが初めて実行されると、Jestは次のようなスナップショットファイルを作成します。
exports[`renders correctly 1`] = `
<a
className="normal"
href="https://#"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;
スナップショットアーティファクトは、コードの変更と一緒にコミットし、コードレビュープロセスの一部としてレビューする必要があります。 Jestはpretty-formatを使用して、コードレビュー中にスナップショットを人間が読めるようにします。 後続のテスト実行では、Jestはレンダリングされた出力を以前のスナップショットと比較します。 一致する場合、テストは合格です。 一致しない場合、テストランナーはコード(この場合は<Link>
コンポーネント)に修正する必要があるバグを発見したか、実装が変更され、スナップショットを更新する必要があります。
スナップショットは、レンダリングするデータに直接スコープされます。この例では、page
プロパティが渡された<Link>
コンポーネントです。 つまり、他のファイルに<Link>
コンポーネントに不足しているプロパティ(たとえば、App.js
)があっても、テストは<Link>
コンポーネントの使用方法を知らず、Link.js
のみにスコープされているため、テストは合格します。 また、他のスナップショットテストで異なるプロパティを使用して同じコンポーネントをレンダリングしても、テストはお互いを知らないため、最初のテストには影響しません。
スナップショットテストの仕組みと、私たちがそれを構築した理由の詳細については、リリースブログ投稿をご覧ください。 スナップショットテストをいつ使用すべきかを知りたい場合は、このブログ投稿を読むことをお勧めします。 また、Jestを使用したスナップショットテストに関するこのeggheadビデオを視聴することをお勧めします。
スナップショットの更新
バグが導入された後にスナップショットテストが失敗した場合は、簡単にわかります。 その場合は、問題を修正し、スナップショットテストが再び合格していることを確認してください。 さて、意図的な実装変更が原因でスナップショットテストが失敗した場合について説明しましょう。
このような状況の1つは、例の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-jest
とそのno-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`;
よくある質問
継続的インテグレーション(CI)システムではスナップショットは自動的に書き込まれますか?
いいえ、Jest 20以降、Jestのスナップショットは、--updateSnapshot
を明示的に渡さずにCIシステムでJestを実行しても自動的に書き込まれません。すべてのスナップショットはCIで実行されるコードの一部であると想定されており、新しいスナップショットは自動的に合格するため、CIシステムでのテスト実行には合格しません。常にすべてのスナップショットをコミットし、バージョン管理下に置いておくことをお勧めします。
スナップショットファイルはコミットする必要がありますか?
はい、すべてのスナップショットファイルは、対象となるモジュールとそのテストと一緒にコミットする必要があります。Jestの他のアサーションの値と同様に、テストの一部と見なす必要があります。実際、スナップショットは、任意の時点におけるソースモジュールの状態を表しています。このように、ソースモジュールが変更された場合、Jestは以前のバージョンから何が変更されたかを判断できます。また、コードレビュー中に多くの追加のコンテキストを提供し、レビュー担当者は変更内容をよりよく理解できます。
スナップショットテストはReactコンポーネントでのみ機能しますか?
ReactとReact Nativeコンポーネントは、スナップショットテストの良いユースケースです。ただし、スナップショットはシリアライズ可能な値をキャプチャできるため、出力が正しいかどうかをテストすることが目標であればいつでも使用する必要があります。Jestリポジトリには、Jest自体の出力、Jestのアサーションライブラリの出力、およびJestコードベースのさまざまな部分からのログメッセージをテストする多くの例が含まれています。JestリポジトリのCLI出力のスナップショットの例を参照してください。
スナップショットテストとビジュアルリグレッションテストの違いは何ですか?
スナップショットテストとビジュアルリグレッションテストは、UIをテストする2つの異なる方法であり、それぞれ異なる目的を果たします。ビジュアルリグレッションテストツールは、Webページのスクリーンショットを撮り、結果の画像をピクセル単位で比較します。スナップショットテストでは、値がシリアライズされ、テキストファイルに格納され、差分アルゴリズムを使用して比較されます。考慮すべきトレードオフはいくつかあり、Jestブログにスナップショットテストが構築された理由を挙げました。
スナップショットテストは単体テストを置き換えますか?
スナップショットテストは、Jestに付属する20以上のアサーションの1つにすぎません。スナップショットテストの目的は、既存の単体テストを置き換えることではなく、追加の価値を提供し、テストを容易にすることです。場合によっては、スナップショットテストにより、特定の機能セット(例:Reactコンポーネント)の単体テストの必要性がなくなる可能性がありますが、単体テストと連携することもできます。
生成されるファイルの速度とサイズに関するスナップショットテストのパフォーマンスはどうですか?
Jestはパフォーマンスを念頭に置いて書き直されており、スナップショットテストも例外ではありません。スナップショットはテキストファイルに格納されるため、このテスト方法は高速で信頼性があります。Jestは、toMatchSnapshot
マッチャーを呼び出すテストファイルごとに新しいファイルを生成します。スナップショットのサイズは非常に小さく、参考までに、Jestコードベース自体にあるすべてのスナップショットファイルのサイズは300 KB未満です。
スナップショットファイル内の競合をどのように解決しますか?
スナップショットファイルは、常に対象となるモジュールの現在の状態を表している必要があります。したがって、2つのブランチをマージしてスナップショットファイルで競合が発生した場合は、競合を手動で解決するか、Jestを実行して結果を確認することでスナップショットファイルを更新できます。
スナップショットテストでテスト駆動開発の原則を適用することはできますか?
スナップショットファイルを手動で作成することは可能ですが、通常は現実的ではありません。スナップショットは、最初にコードを設計するためのガイダンスを提供するのではなく、テスト対象のモジュールの出力が変更されたかどうかを判断するのに役立ちます。
コードカバレッジはスナップショットテストで機能しますか?
はい、他のテストと同様に機能します。