スナップショットテスト
スナップショットのテストはUI が予期せず変更されていないかを確かめるのに非常に有用なツールです。
典型的なスナップショットテストでは、UIコンポーネントをレンダリングし、スナップショットを撮り、テストと一緒に保管されているスナップショットファイルと比較します。 2つのスナップショットが一致しない場合テストは失敗します: 予期されない変更があったか、参照するスナップショットが新しいバージョンのUIコンポーネントに更新される必要があるかのどちらかです。
Jestにおけるスナップショットテスト
React コンポーネントをテストする場合にも、同様のア プローチをとることができます。 アプリケーション全体の構築が必要となるグラフィカルなUIをレンダリングする代わりに、シリアライズ可能なReactツリーの値を素早く生成するテスト用レンダラーを利用できます。 Consider this example test for a Link component:
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();
});
The first time this test is run, Jest creates a snapshot file that looks like this:
exports[`renders correctly 1`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;
生成されるスナップショットはコードの変更に追随し、かつコードレビューのプロセスの一部としてレビューされるべきです。 Jest uses pretty-format to make snapshots human-readable during code review. On subsequent test runs, Jest will compare the rendered output with the previous snapshot. それらが一致すれば、テストを通過します。 If they don't match, either the test runner found a bug in your code (in the <Link>
component in this case) that should be fixed, or the implementation has changed and the snapshot needs to be updated.
The snapshot is directly scoped to the data you render – in our example the <Link>
component with page
prop passed to it. This implies that even if any other file has missing props (say, App.js
) in the <Link>
component, it will still pass the test as the test doesn't know the usage of <Link>
component and it's scoped only to the Link.js
. Also, rendering the same component with different props in other snapshot tests will not affect the first one, as the tests don't know about each other.
スナップショットのテストのしくみ、およびそれを作成した理由の詳細については、 release blog postで読むことができます。 いつスナップショットテストを使用するべきかについて、よい感覚を身に付けるには、このブログ記事を読むことをお勧めします。 Jestでスナップショットテストを行うこの eggheadの動画 も観ることをお勧めします。 (訳注: 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
フラグを追加して指定することでパターンにマッチするテストのみスナップショットを再生成することができます。
You can try out this functionality by cloning the snapshot example, modifying the Link
component, and running 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
オプションや、 u
キーを --watch
モードで使用することでスナップショットを更新することもできます。
By default, Jest handles the writing of snapshots into your source code. However, if you're using prettier in your project, Jest will detect this and delegate the work to prettier instead (including honoring your configuration).
Property Matchers
Often there are fields in the object you want to snapshot which are generated (like IDs and Dates). If you try to snapshot these objects, they will force the snapshot to fail on every run:
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",
}
`;
For these cases, Jest allows providing an asymmetric matcher for any property. These matchers are checked before the snapshot is written or tested, and then saved to the snapshot file instead of the received value:
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',
}
`;
If the case concerns a string not an object then you need to replace random part of that string on your own before testing the snapshot.
You can use for that e.g. replace()
and regular expressions.
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();
Other ways this can be done is using the snapshot serializer or mocking the library responsible for generating the random part of the code you're snapshotting.
ベストプラクティス
スナップショットは、アプリケーション内で予期しないインターフェイスの変更を特定するための素晴らしいツールです。 UI、ログ、またはエラーメッセージのいずれであってもです。 あらゆるテスト戦略と同様に、知っておくべきベストプラクティスと、それらを効果的に使用するために、遵守すべきガイドラインがあります。
1. スナップショットをコードとして扱いましょう
スナップショットをコミットし、通常のコードレビュープロセスの一部としてレビューします。 これは、プロジェクト内の他の種類のテストやコードと同様にスナップショットを扱うことを意味します。
スナップショットは、範囲を絞り込んで短くするべきで、これらのスタイル規約を強制することで、可読性を確保するよう心がけてください。
As mentioned previously, Jest uses pretty-format
to make snapshots human-readable, but you may find it useful to introduce additional tools, like eslint-plugin-jest
with its no-large-snapshots
option, or snapshot-diff
with its component snapshot comparison feature, to promote committing short, focused assertions.
目標は、プルリクエストでスナップショットを簡単に確認できるようにすることです。 そしてテストスイートが失敗したときに失敗の根本原因を調べず、スナップショットを再作成する習慣と戦うためです。
2. べき等性のあるテストを書きましょう
テストは確定的なものであるべきです。 変更がないコンポーネントに対して同じテストを複数回実施しても毎回同じ結果が得られるべきなのです。 生成したスナップショットがプラットフォームに固有のものやその他の非確定的なデータを含まないように努めなければなりません。
For example, if you have a Clock component that uses Date.now()
, the snapshot generated from this component will be different every time the test case is run. このケースでは 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>
`;
Since the latter describes exactly what's expected in the output, it's more clear to see when it's wrong:
exports[`<UserName /> should render null`] = `
<div>
Alan Turing
</div>
`;
exports[`<UserName /> should render Alan Turing`] = `null`;
よくある質問
スナップショットは継続的インテグレーションシステム(CI) では自動的に生成されないのでしょうか?
Jestバージョン20では、明示的に --updateSnapshot
を指定しない限り、CIシステムでJestを実行してもJest内のスナップショットは生成されません。 全てのスナップショットはCI上で実行されるコードの一部であることが期待され、新しいスナップショットは自動的にパスしているはずなので、CIシステム上のテストをパスするか確認するべきではないのです。 全てのスナップショットをコミットしてバージョン管理することをお勧めします。
スナップショットファイルはコミットする必要がありますか?
はい、スナップショットがカバーするモジュールとテストと共にすべてのスナップショットファイルはコミットされるべきです。 Jestの他のアサーションの値と同様に、スナップショットはテストの一部とみなされるべきです。 実際、スナップショットが指定された時点でのソースモジュールの状態を表すものなのです。 こうしてソースモジュールが変更された場合、Jestは以前のバージョンから変更があったことを見分けられるのです。 コードレビューにおいてレビュアーが加えられた変更をより理解しやすくなるたくさんの追加のコンテクストを提供するものでもあります。