Snapshot Testing
スナップショットのテストは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は以下のような結果を出力します。
異なるアドレスを指すようにコンポーネントを更新したのですから、このコンポーネントのスナップショットに変更があると予想するのが妥当でしょう。 更新されたコンポーネントのスナップショットは今やこのテストで生成されたスナップショットと一致しないので、スナップショットのテストケースは失敗します。
To resolve this, we will need to update our snapshot artifacts. You can run Jest with a flag that will tell it to re-generate snapshots:
jest --updateSnapshot
上記のコマンドを実行することで変更を受け入れることができます。 お好みで一文字の -u
フラグでもスナップショットの再生成を行うことができます。 このフラグは失敗する全てのスナップショットテストのスナップショットを再生成します。 意図しないバグにより追加されたスナップショットテストの失敗があれば、バグが混ざった状態でスナップショットを記録することを避けるためにスナップショットを再生成する前にバグを修正する必要があります。
再生成されるスナップショットを限定したい場合は、 --testNamePattern
フラグを追加して指定することでパターンにマッチするテストのみスナップショットを再生成することができます。
この機能を試すにはsnapshot example リポジトリをクローンして Link
コンポーネントを変更してJestを実行してみて下さい。
ベストプラクティス
スナップショットは、アプリケーション内で予期しないインターフェイスの変更を特定するための素晴らしいツールです。 UI、ログ、またはエラーメッセージのいずれであってもです。 あらゆるテスト戦略と同様に、知っておくべきベストプラクティスと、それらを効果的に使用するために、遵守すべきガイドラインがあります。
1. Treat snapshots as code
Commit snapshots and review them as part of your regular code review process. This means treating snapshots as you would any other type of test or code in your project.
スナップショットは、範囲を絞り込んで短くするべきで、これらのスタイル規約を強制することで、可読性を確保するよう心がけてください。
前述のように、Jestは可読性のためにpretty-format
を使用していますが、より明快なテストを作成するために、eslint-plugin-jest
をno-large-snapshots
オプションやsnapshot-diff
のコンポーネントスナップショット機能を利用したくなることもあるでしょう。
目標は、プルリクエストでスナップショットを簡単に確認できるようにすることです。そしてテストスイートが失敗したときに失敗の根本原因を調べず、スナップショットを再作成する習慣と戦うためです。
2. Tests should be deterministic
テストは確定的なものであるべきです。 変更がないコンポーネントに対して同じテストを複数回実施しても毎回同じ結果が得られるべきなのです。 生成したスナップショットがプラットフォームに固有のものやその他の非確定的なデータを含まないように努めなければなりません。
例えばDate.now()
を利用する Clock コンポーネントがあれば、このコンポーネントから生成されるスナップショットは テストケースが実行されるごとに異なるでしょう。 このケースでは Date.now() メソッドをモックすることでテストを実行するごとに一貫した値を返すようにできます。
Date.now = jest.fn(() => 1482363367071);
これで スナップショットテストを実行するごとに、Date.now()
は一貫して1482363367071
を返すようになりました。 これにより、いつテストを実行したかに関係なく、このコンポーネントに生成されるスナップショットは同じ結果となります。
3. Use descriptive snapshot names
スナップショットには、常に叙述的なテストやスナップショット名を使用するようにしてください。 ベストな命名は、期待されるスナップショットの内容を述るものにすることです。 これにより、レビュー中にレビュアーがスナップショットを確認しやすくなります。 更新前のスナップショットが正しい動作であるかどうかを、誰でも知ることができます。
例えば、以下を比べてみましょう:
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を実行してスナップショットを更新して結果を確認することができます。
スナップショットテストにテスト駆動開発の原則を適用することはできますか?
手動でスナップショットを作成することもできますが、大抵はやりやすいものではありません。 スナップショットは最初期の段階でコード設計の手引きとなるよりも、テスト対象のモジュールの出力が変更されたかを分かりやすくするものです。
コードカバレッジはスナップショットテストでも機能しますか?
はい、他のテストと同様です。