Jest再入門 - スナップショットテスト編

| 6 min read
Author: noboru-kudo noboru-kudoの画像

スナップショットテストはJestオリジナルの機能です。
公式ドキュメントによると以下の説明があります。

Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly.
A typical snapshot test case 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ドキュメント - Snapshot Testing

スナップショットテストは、ReactやVue等のコンポーネントで生成するUIが、前回実行時と変わっていないかをチェックします。
UIは機能追加や改善等で変更頻度が高い領域です。しかし、デザイン等を含めた全要素を単体テストで網羅するのはコストメリットに見合わないというのが実情です。
このようなケースで、以前の出力結果と全比較するスナップショットテストは効果を発揮します。
もちろん、実装変更によって出力結果は変わっていきますので、スナップショットテストの失敗は必ずしも実装バグにはつながりません。
差分を人間の目でチェックし、問題がない場合はスナップショットを更新していく必要があります。

スナップショットテストのスコープはUIコンポーネントに限りません。比較できるものであれば基本的には何でも利用できます。
とはいえ、スナップショットテストを乱用するとテスト観点がボヤけてカオスな状況になります。利用ケースは各システムの特性に合わせて限定するのが望ましいでしょう。

ここでは、そんなスナップショットテストのやり方を見ていきます。

Contents

基本的な使い方

#

スナップショットテストもJestが用意しているマッチャーの1つです。基本的なテストの記述方法は他のテストと変わりません。
通常は各UIコンポーネント等で生成した結果を検査対象としますが、ここでは簡易的に固定のHTML文字列を対象とします。

describe("Snapshot Testing", () => {
test("toMatchSnapshot - 基本", () => {
const html = `<div class="container">
<article>
<p>UI生成結果</p>
</article>
</div>
`
;
expect(html).toMatchSnapshot();
});
});

スナップショットテストはJestマッチャーとして提供されるtoMatchSnapshotを利用します。
これを初めて実行すると、テストは成功します。これは前回のテスト実行結果がないためです。
このとき、テストファイルが配置されている場所にスナップショットファイル(__snapshots__/<テストファイル名>.snap)が作成されます。
上記テストの場合は、以下の内容になります。

exports[`Snapshot Testing toMatchSnapshot - 基本 1`] = `
"<div class=\\"container\\">
  <article>
    <p>UI生成結果</p>
  </article>
</div>"
`;

2回目以降の実行では、このスナップショットファイルと比較するようになります。

意図的にテストを失敗させてみます。
以下のようにpタグにclass属性titleを追加します。

    const html = `<div class="container">
<article>
<p class="title">UI生成結果</p>
</article>
</div>
`
;

CLIでテストを実行すると、テストが失敗して以下のように出力されます。

  ● Snapshot Testing › toMatchSnapshot - 基本

expect(received).toMatchSnapshot()

Snapshot name: `Snapshot Testing toMatchSnapshot - 基本 1`

- Snapshot - 1
+ Received + 1

<div class="container">
<article>
- <p>UI生成結果</p>
+ <p class="title">UI生成結果</p>
</article>
</div>

8 | </article>
9 | </div>`;
> 10 | expect(html).toMatchSnapshot();
| ^
11 | });

1 snapshot failed.

先程生成されたスナップショットと結果を比較して、pタグのclass属性で差分がでていることが分かります。
今回はデグレではなく、これが正しい差分であるとします。
この場合はスナップショットファイルを更新する必要があります。スナップショットファイルは手動更新もできますが、通常はJestの機能を使って更新します。

npx jest --test-match="**/snapshot.spec.ts" --update-snapshot

実行するとテストが成功し、スナップショットファイルが更新されます。次回以降も出力結果が変わらなければ、テストは成功するようになります。

もちろん、スナップショットファイルはGit管理の対象とし、変更された場合はコードレビューを通して妥当なものであるかをチェックする必要があります。
スナップショットの変更をチェックしなければ、スナップショットテストの意味は全くありません。

Information

スナップショットファイルの更新は、VSCodeのJest Extension(vscode-jest)やIntellij IDEAでも用意されています。
各種IDEではスナップショットファイルの該当箇所へのジャンプ等、便利な機能が用意されていますので、通常の開発では、CLIよりもこちらを利用することが多いと思います。

インラインスナップショット

#

上記は、テスト実行時にスナップショットファイルを作成しましたが、インラインスナップショットを使ってテストコード内に埋め込むこともできます。
インラインスナップショットを使う場合は、以下のように記述します。

test("toMatchSnapshot - インライン", () => {
const html = `<div class="container">
<article>
<p>UI生成結果</p>
</article>
</div>
`
;
expect(html).toMatchInlineSnapshot();
});

インラインスナップショットの場合は、toMatchInlineSnapshotマッチャーを使います。
この状態でテストを実行すると、Jestはテストコードを以下のように直接書き換えます。

test("toMatchSnapshot - インライン", () => {
const html = `<div class="container">
<article>
<p>UI生成結果</p>
</article>
</div>
`
;
expect(html).toMatchInlineSnapshot(`
"<div class=\\"container\\">
<article>
<p>UI生成結果</p>
</article>
</div>"
`
);
});

toMatchInlineSnapshotの引数にスナップショットが埋め込まれました。
スナップショットの更新は、先程と同様ですが、スナップショットファイルではなく、直接テストコードが書き換えられます。

検査対象が小さい場合は、こちらを利用するのが簡単でしょう。

プロパティマッチャー

#

タイムスタンプやID等の自動生成系のもの等、実行の都度値が変わるものが含まれる場合には、そのままではスナップショットテストは使えません。
一般的には、このような場合はモックを使用して、その値を固定化する必要があります。
検査対象がオブジェクト等のキーバリュー形式であれば、モック化せずともJestのプロパティマッチャーが利用できます。

ここでは、JavaScriptのオブジェクトをスナップショットテストしています。
その中にはランダム値(UUID)やタイムスタンプが含まれるものとします。
プロパティマッチャーは以下のように記述します。

test("toMatchSnapshot - Property Matchers", () => {
const obj = {
id: uuidv4(),
created: new Date().getTime(),
type: "Jest",
};
expect(obj).toMatchSnapshot({
id: expect.stringMatching(
/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/
),
created: expect.any(Number),
});
});

toMatchSnapshotの引数で利用されているのがプロパティマッチャーです。
ここで、等価条件以外で比較したいものを記述します。上記ではランダム値のIDは正規表現でUUIDフォーマットであるか、タイムスタンプはNumber型であることを検査するように記述しています。
なお、ここで記述していないもの以外(上記だとtypeフィールド)は、等価条件で比較されます。

この場合のスナップショットファイルは、以下のようになります。

exports[`Snapshot Testing toMatchSnapshot - Property Matchers 1`] = `
Object {
  "created": Any<Number>,
  "id": StringMatching /\\^\\[\\\\da-f\\]\\{8\\}-\\[\\\\da-f\\]\\{4\\}-\\[\\\\da-f\\]\\{4\\}-\\[\\\\da-f\\]\\{4\\}-\\[\\\\da-f\\]\\{12\\}\\$/,
  "type": "Jest",
}
`;

等価条件以外の部分が、プロパティマッチャーに置き換えられていることが分かります。

利用シーンは限定されますが、APIレスポンスやReact Test Renderer等、検査対象をJSON形式に変換可能である場合に力を発揮します。


次回は関数・モジュールモック編に続きます。


関連記事


参照資料

豆蔵デベロッパーサイト - 先週のアクセスランキング
  1. Nuxt3入門(第1回) - Nuxtがサポートするレンダリングモードを理解する (2022-09-25)
  2. 自然言語処理初心者が「GPT2-japanese」で遊んでみた (2022-07-08)
  3. GitHub Codespaces を使いはじめる (2022-05-18)
  4. Jest再入門 - 関数・モジュールモック編 (2022-07-03)
  5. ORマッパーのTypeORMをTypeScriptで使う (2022-07-27)
  6. Nuxt3入門(第4回) - Nuxtのルーティングを理解する (2022-10-09)
  7. Nuxt3入門(第3回) - ユニバーサルフェッチでデータを取得する (2022-10-06)
  8. 第1回 OpenAPI Generator を使ったコード生成 (2022-06-04)
  9. Nuxt3入門(第8回) - Nuxt3のuseStateでコンポーネント間で状態を共有する (2022-10-28)
  10. Nuxt3入門(第2回) - 簡単なNuxtアプリケーションを作成する (2022-10-02)