Snapshot Testing
スナップショットのテストはUI が予期せず変更されていないかを確かめるのに非常に有用なツールです。
A typical snapshot test case for a mobile app renders a UI component, takes a snapshot, then compares it to a reference snapshot file stored alongside the test. The test will fail if the two snapshots do not match: either the change is unexpected, or the reference snapshot needs to be updated to the new version of the UI component.
Jestにおけるスナップショットテスト
Reactコンポーネントをテストする場合には同様のアプローチをとる事ができます。 アプリケーション全体の構築が必要となるグラフィカルなUIをレンダリングする代わりに、シリアライズ可能なReactツリーの値を素早く生成するテスト用レンダラーを利用できます。 Consider this example test for a Link component:
import React from 'react';
import Link from '../Link.react';
import renderer from 'react-test-renderer';
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 this case, it's <Link>
component) 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 it's
<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 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.
More information on how snapshot testing works and why we built it can be found on the 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を実行してみて下さい。
インタラクティブ・スナップショットモード
失敗したスナップショットは、ウォッチモードで対話的に更新することもできます。
Once you enter Interactive Snapshot Mode, Jest will step you through the failed snapshots one test at a time and give you the opportunity to review the failed output.
ここで、スナップショットを更新するか、次にスキップするかを選択できます。
Once you're finished, Jest will give you a summary before returning back to watch mode:
Inline Snapshots
Inline snapshots behave identically to external snapshots (.snap
files), except the snapshot values are written automatically back into the source code. This means you can get the benefits of automatically generated snapshots without having to switch to an external file to make sure the correct value was written.
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.
例:
First, you write a test, calling .toMatchInlineSnapshot()
with no arguments:
it('renders correctly', () => {
const tree = renderer
.create(<Link page="https://prettier.io">Prettier</Link>)
.toJSON();
expect(tree).toMatchInlineSnapshot();
});
The next time you run Jest, tree
will be evaluated, and a snapshot will be written as an argument to 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>
`);
});
That's all there is to it! You can even update the snapshots with --updateSnapshot
or using the u
key in --watch
mode.
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",
}
`;
Any given value that is not a matcher will be checked exactly and saved to the snapshot:
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',
}
`;
Best Practices
Snapshots are a fantastic tool for identifying unexpected interface changes within your application – whether that interface is an API response, UI, logs, or error messages. As with any testing strategy, there are some best-practices you should be aware of, and guidelines you should follow, in order to use them effectively.
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.
Ensure that your snapshots are readable by keeping them focused, short, and by using tools that enforce these stylistic conventions.
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.
The goal is to make it easy to review snapshots in pull requests, and fight against the habit of regenerating snapshots when test suites fail instead of examining the root causes of their failure.
2. Tests should be deterministic
テストは確定的なものであるべきです。 Running the same tests multiple times on a component that has not changed should produce the same results every time. 生成したスナップショットがプラットフォームに固有のものやその他の非確定的なデータを含まないように努めなければなりません。
例えばDate.now()
を利用する Clock コンポーネントがあれば、このコンポーネントから生成されるスナップショットは テストケースが実行されるごとに異なるでしょう。 このケースでは Date.now() メソッドをモックすることでテストを実行するごとに一貫した値を返すようにできます。
Date.now = jest.fn(() => 1482363367071);
これで スナップショットテストを実行するごとに、Date.now()
は一貫して1482363367071
を返すようになりました。 これにより、いつテストを実行したかに関係なく、このコンポーネントに生成されるスナップショットは同じ結果となります。
3. Use descriptive snapshot names
Always strive to use descriptive test and/or snapshot names for snapshots. The best names describe the expected snapshot content. This makes it easier for reviewers to verify the snapshots during review, and for anyone to know whether or not an outdated snapshot is the correct behavior before updating.
For example, compare:
exports[`<UserName /> should handle some test case`] = `null`;
exports[`<UserName /> should handle some other test case`] = `
<div>
Alan Turing
</div>
`;
To:
exports[`<UserName /> should render null`] = `null`;
exports[`<UserName /> should render Alan Turing`] = `
<div>
Alan Turing
</div>
`;
Since the later 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`;
よくある質問
Are snapshots written automatically on Continuous Integration (CI) systems?
No, as of Jest 20, snapshots in Jest are not automatically written when Jest is run in a CI system without explicitly passing --updateSnapshot
. 全てのスナップショットはCI上で実行されるコードの一部であることが期待され、新しいスナップショットは自動的にパスしているはずなので、CIシステム上のテストをパスするか確認するべきではないのです。 全てのスナップショットをコミットしてバージョン管理することをお勧めします。
スナップショットファイルはコミットする必要がありますか?
はい、スナップショットがカバーするモジュールとテストと共にすべてのスナップショットファイルはコミットされるべきです。 They should be considered part of a test, similar to the value of any other assertion in Jest. 実際、スナップショットが指定された時点でのソースモジュールの状態を表すものなのです。 こうしてソースモジュールが変更された場合、Jestは以前のバージョンから変更があったことを見分けられるのです。 コードレビューにおいてレビュアーが加えられた変更をより理解しやすくなるたくさんの追加のコンテクストを提供するものでもあります。
スナップショットテストはReactコンポーネントでのみ利用できますか?
React と React Nativeコンポーネントはスナップショットテストを行うのに良いユースケースです。 しかしスナップショットは任意のシリアライズ可能な値をキャプチャでき、出力が正しいかをテストするという目的に対していつでも利用できるべきです。 JestのリポジトリにはJest自身のテスト結果の例や、アサーションのライブラリ、そしてコードベースの様々な部分におけるログメッセージも同様に含まれています。 Jestのリポジトリのsnapshotting CLI output の例を参照してください。
スナップショットテストとビジュアルの回帰テストの違いは何ですか?
スナップショットテストとビジュアルの回帰テストはUIをテストする2つの独立した方法であり、目的が異なります。 ビジュアルの回帰テストツールはwebページのスクリーンショットを取得して出力された画像をピクセル単位で比較します。 With Snapshot testing values are serialized, stored within text files, and compared using a diff algorithm. There are different trade-offs to consider and we listed the reasons why snapshot testing was built in the Jest blog.
Does snapshot testing replace unit testing?
スナップショットテストはJestに含まれる20以上のアサーションの1つに過ぎません。 The aim of snapshot testing is not to replace existing unit tests, but to provide additional value and make testing painless. 一部のシナリオではスナップショットテストは特定の機能セット(例: Reactコンポーネント)における単体テストの必要性を取り去る可能性がありますが、並行して利用することもできます。
生成されたファイルのサイズと処理速度についてのスナップショットテストのパフォーマンスはどうですか?
Jestはパフォーマンスを念頭に置いた修正を実施し続けており、スナップショットテストも例外ではありません。 スナップショットはテキストファイルに保管されるので、テストは高速で信頼性が高いものになります。 Jestは toMatchSnapshot
マッチャを呼び出す各テストファイルごとに新しいファイルを生成します。 The size of the snapshots is pretty small: For reference, the size of all snapshot files in the Jest codebase itself is less than 300 KB.
スナップショットファイル内での競合を解決するには?
スナップショットファイルは対象とするモジュールの現在の状態を表すものでなければなりません。 Therefore, if you are merging two branches and encounter a conflict in the snapshot files, you can either resolve the conflict manually or update the snapshot file by running Jest and inspecting the result.
スナップショットテストにテスト駆動開発の原則を適用することはできますか?
手動でスナップショットを作成することもできますが、大抵はやりやすいものではありません。 Snapshots help to figure out whether the output of the modules covered by tests is changed, rather than giving guidance to design the code in the first place.
Does code coverage work with snapshot testing?
Yes, as well as with any other test.