スナップショットテスト
スナップショットのテストはUI が予期せず変更されていないかを確かめるのに非常に有用なツールです。
A typical snapshot test case renders a UI component, takes a snapshot, then compares it to a reference snapshot file stored alongside the test. 2つのスナップショットが一致しない場合テストは失敗します: 予期されない変更があったか、参照するスナップショットが新しいバージョンのUIコンポーネントに更新される必要があるかのどちらかです。
Jestにおけるスナップショットテスト
Reactコンポーネントをテストする場合には同様のアプローチをとる事ができます。 アプリケーション全体の構築が必要となるグラフィカルなUIをレンダリングする代わりに、シリアライズ可能なReactツリーの値を素早く生成するテスト用レンダラーを利用できます。 以下のLink componentのテスト例について考えてみましょう:
import React from 'react';
import renderer from 'react-test-renderer';
import Link from '../Link.react';
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を利用します。 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.
Note: The snapshot is directly scoped to the data you render – in our example the
<Link />
component withpage
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 theLink.react.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. ioというJavaScript学習サイトの事を指していると思われます)
スナップショットの更新
バグが混入した後でスナップショットテストが失敗したときは簡単に目星がつきます。 テストが失敗したら、その原因箇所に向かって問題を修正し、スナップショットテストが再びパスすることを確認すればよいのです。 ここで、意図的な仕様変更によりスナップショットテストが失敗するケースについて議論しましょう。
このような状況はたとえば以下の例の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
フラグを追加して指定することでパターンにマッチするテストのみスナップショットを再生成することができます。
この機能を試すにはsnapshot example リポジトリをクローンして Link
コンポーネントを変更してJestを実行してみて下さい。
インタラクティブ・スナップショットモード
失敗したスナップショットは、ウォッチモードで対話的に更新することもできます。
インタラクティブ・スナップショットモードに入ると、Jest は一度に1つのテストごとに、失敗したスナップショットをステップ実行させてくれます。ここで、失敗した出力を確認できます。
ここで、スナップショットを更新するか、次にスキップするかを選択できます。
終了したら、Jest はウォッチモードに戻る前に概要を表示します。
インラインスナップショット
インラインスナップショットは外部スナップショット(.snap
ファイル)と同じように動作しますが、スナップショットした値は自動的にソースコードに書き戻されます。 つまり、外部ファイルに切り替えて正しい値が書き込まれていることを確認することなく、自動的に生成されたスナップショットの利点を得ることができます。
Inline snapshots are powered by Prettier. To use inline snapshots you must have
prettier
installed in your project. Your Prettier configuration will be respected when writing to test files.If you have
prettier
installed in a location where Jest can't find it, you can tell Jest how to find it using the"prettierPath"
configuration property.
例:
まず、テストを記述し、引数なしで .toMatchInlineSnapshot()
を呼び出します。
it('renders correctly', () => {
const tree = renderer
.create(<Link page="https://prettier.io">Prettier</Link>)
.toJSON();
expect(tree).toMatchInlineSnapshot();
});
次回Jestを実行した際には、tree
が評価され、スナップショットが記述されてtoMatchInlineSnapshot
の引数となります。
it('renders correctly', () => {
const tree = renderer
.create(<Link page="https://prettier.io">Prettier</Link>)
.toJSON();
expect(tree).toMatchInlineSnapshot(`
<a
className="normal"
href="https://prettier.io"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Prettier
</a>
`);
});
これだけです! --updateSnapshot
オプションや、 u
キーを --watch
モードで使用することでスナップショットを更新することもできます。
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`] = `
Object {
"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`] = `
Object {
"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`] = `
Object {
"createdAt": Any<Date>,
"name": 'Bond... James Bond',
}
`;
ベストプラクティス
スナップショットは、アプリケーション内で予期しないインターフェイスの変更を特定するための素晴らしいツールです。 UI、ログ、またはエラーメッセージのいずれであってもです。 あらゆるテスト戦略と同様に、知っておくべきベストプラクティスと、それらを効果的に使用するために、遵守すべきガイドラインがあります。
1. スナップショットをコードとして扱いましょう
スナップショットをコミットし、通常のコードレビュープロセスの一部としてレビューします。 これは、プロジェクト内の他の種類のテストやコードと同様にスナップショットを扱うことを意味します。
スナップショットは、範囲を絞り込んで短くするべきで、これらのスタイル規約を強制することで、可読性を確保するよう心がけてください。
前述のように、Jestは可読性のためにpretty-format
を使用していますが、より明快なテストを作成するために、eslint-plugin-jest
をno-large-snapshots
オプションやsnapshot-diff
のコンポーネントスナップショット機能を利用したくなることもあるでしょう。
目標は、プルリクエストでスナップショットを簡単に確認できるようにすることです。そしてテストスイートが失敗したときに失敗の根本原因を調べず、スナップショットを再作成する習慣と戦うためです。
2.べき等性のあるテストを書きましょう
テストは確定的なものであるべきです。 変更がないコンポーネントに対して同じテストを複数回実施しても毎回同じ結果が得られるべきなのです。 生成したスナップショットがプラットフォームに固有のものやその他の非確定的なデータを含まないように努めなければなりません。
例えばDate.now()
を利用する Clock コンポーネントがあれば、このコンポーネントから生成されるスナップショットは テストケースが実行されるごとに異なるでしょう。 このケースでは Date.now() メソッドをモックすることでテストを実行するごとに一貫した値を返すようにできます。
Date.now = jest.fn(() => 1482363367071);
これで スナップショットテストを実行するごとに、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では、明示的に --updateSnapshot
を指定しない限り、CIシステムでJestを実行してもJest内のスナップショットは生成されません。 全てのスナップショットはCI上で実行されるコードの一部であることが期待され、新しいスナップショットは自動的にパスしているはずなので、CIシステム上のテストをパスするか確認するべきではないのです。 全てのスナップショットをコミットしてバージョン管理することをお勧めします。
スナップショットファイルはコミットする必要がありますか?
はい、スナップショットがカバーするモジュールとテストと共にすべてのスナップショットファイルはコミットされるべきです。 Jestの他のアサーションの値と同様に、スナップショットはテストの一部とみなされるべきです。 実際、スナップショットが指定された時点でのソースモジュールの状態を表すものなのです。 こうしてソースモジュールが変更された場合、Jestは以前のバージョンから変更があったことを見分けられるのです。 コードレビューにおいてレビュアーが加えられた変更をより理解しやすくなるたくさんの追加のコンテクストを提供するものでもあります。
スナップショットテストはReactコンポーネントでのみ利用できますか?
React と React Nativeコンポーネントはスナップショットテストを行うのに良いユースケースです。 しかしスナップショットは任意のシリアライズ可能な値をキャプチャでき、出力が正しいかをテストするという目的に対していつでも利用できるべきです。 JestのリポジトリにはJest自身のテスト結果の例や、アサーションのライブラリ、そしてコードベースの様々な部分におけるログメッセージも同様に含まれています。 Jestのリポジトリのsnapshotting CLI output の例を参照してください。
スナップショットテストとビジュアルの回帰テストの違いは何ですか?
スナップショットテストとビジュアルの回帰テストはUIをテストする2つの独立した方法であり、目的が異なります。 ビジュアルの回帰テストツールはwebページのスクリーンショットを取得して出力された画像をピクセル単位で比較します。 スナップショットテストにおいてはシリアライズされた値をテキストファイルに格納して、異なるアルゴリズムで比較します。 There are different trade-offs to consider and we listed the reasons why snapshot testing was built in the Jest blog.
スナップショットテストは単体テストを代替するものですか?
スナップショットテストはJestに含まれる20以上のアサーションの1つに過ぎません。 スナップショットテストのねらいは既存の単体テストを代替することではなく、追加のテスト結果を提供してテストにおける作業負担を減らすことです。 一部のシナリオではスナップショットテストは特定の機能セット(例: Reactコンポーネント)における単体テストの必要性を取り去る可能性がありますが、並行して利用することもできます。
生成されたファイルのサイズと処理速度についてのスナップショットテストのパフォーマンスはどうですか?
Jestはパフォーマンスを念頭に置いた修正を実施し続けており、スナップショットテストも例外ではありません。 スナップショットはテキストファイルに保管されるので、テストは高速で信頼性が高いものになります。 Jestは toMatchSnapshot
マッチャを呼び出す各テストファイルごとに新しいファイルを生成します。 スナップショットのサイズはかなり小さく: 参考までにJestのコードベースそのもののスナップショットファイルの総合計を例にすれば、300KB未満です。
スナップショットファイル内での競合を解決するには?
スナップショットファイルは対象とするモジュールの現在の状態を表すものでなければなりません。 したがって、2つのブランチをマージしてスナップショットファイル内での競合に出くわしたなら、手動で競合を解決するかJestを実行してスナップショットを更新して結果を確認することができます。
スナップショットテストにテスト駆動開発の原則を適用することはできますか?
手動でスナップショットを作成することもできますが、大抵はやりやすいものではありません。 スナップショットは最初期の段階でコード設計の手引きとなるよりも、テスト対象のモジュールの出力が変更されたかを分かりやすくするものです。
コードカバレッジはスナップショットテストでも機能しますか?
はい、他のテストと同様です。