👻

UIコンポーネントだけじゃないスナップショットテストの使い所

2022/12/14に公開

ログラスでエンジニアをしている龍島です。最近の週末はもっぱらクリスマスに向けて丸鶏を焼く練習をしています。

本記事は株式会社ログラス Productチーム Advent Calendar 2022の14日目の記事です。

ログラスは質とスピードを両立して高品質なプロダクトをユーザに届け続けたいと考えており、テスト手法も場合に適したものを利用しています。その中で最近はスナップショットテストを積極的に取り入れているのですが、web上にあまり使い所の情報がないため、ログラスでの活用例を交えて書いてみます。

スナップショットテストとは

スナップショットテストはテスト対象のモジュールにインプットパターンを与え、アウトプットをシリアライズしたものを保存しておき、変更の前後で変化していないことを保証するテストです。
スナップショットテスト機能を持つ代表的なテストツールであるJestだとこのような感じです。

test.ts
//テスト対象
const sayHello = (name: string) => `Hello, ${name}!`;

test("挨拶の文字列が生成される", () => {
  const name = "Makima"
  const result = sayHello(name)
  expect(result).toMatchSnapshot()
})
__snapshots__/test.ts.snap
exports[`挨拶の文字列が生成される 1`] = `"Hello, Makima!"`;

※デモとしてスナップショットを用いていますが、あまり適している場面ではありません。適する場面は後述します。

初回実行時に別ファイルに実行結果のテキストが保存され、2回目以降は差分がないかチェックされます。
通常のアサーションをするテストと比較して期待値の生成をテストツールに任せるのが特徴です。

スナップショットテストと聞くとReactなどでUIコンポーネントの出力をテストすることが思い浮かぶ方が多いと思います。Jestの紹介ページにも下記のようにあります。
https://jestjs.io/docs/snapshot-testing

A typical snapshot test case renders a UI component, takes a snapshot, then compares it to a reference snapshot file stored alongside the test.

しかしUIコンポーネント以外にもスナップショットテストが有効な場面は多いです。特徴を整理しどのように利用していけばよいか考えていきます。

特徴

スナップショットテストの特徴を捉えるため、そもそもの自動テストの効用を大きく2つに分けて考えてみます。

【動作の妥当性テスト】 意図した通りに動作することを保証する
【リグレッションテスト】 任意の改修前後で動作がデグレードしていないことを保証する

スナップショットテストは動作の妥当性テストの効用は限定的で、リグレッションテストの効用がとても強いです。

リグレッションテストの観点

リグレッションテストの観点におけるスナップショットテストは、作成が低コストで済むという点で強力です。
インプットとアウトプットのアサーションを記述する通常のテストと比較して、スナップショットテストはインプットの記述のみで済み、インプットパターンを網羅性のあるもので整備することに集中できます。

インプットパターンが複雑なモジュール(例えばそれぞれ10種類の値をとりうる引数が3ある関数)に対して、すべての組み合わせ(10*10*10=1000通り)をテストコード上で作成し、アウトプットを保持しておくことなども可能になります。
またモジュールの動作に変更があった場合も、差分を確認した上でスナップショットファイルをアップデートするだけで良いため、自動テストを整備したもののメンテナンスに苦しめられるといった状況も陥りにくいです。

後述の動作の妥当性は別途保証する必要がありますが、リグレッションテスト目的であればコスパに優れた手法と言えます。

動作の妥当性テストの観点

Jestを始め、多くのスナップショットテストツールは一番最初のテスト実行時において、アウトプットがどのようなものであれ成功としてスナップショットを新規に保存します。
よってテスト対象の動作が正しいかどうかは何か別の方法で確認する必要がありますが、方法としては2つが考えられます。

  • 手動テストなど他の方法で動作の妥当性を保証する
  • スナップショットファイルを目で確認し、動作が妥当であると判断する

実行結果のフォーマットを見やすく工夫することで後者の方法も取ることができますが、基本的には他のテストと組み合わせて利用するのが特徴を活かしやすいです。

適している場面、適していない場面

上記を踏まえてスナップショットの使い所を考えます。

  • 別の手動テスト等で既に動作の妥当性が保証されている場面
  • インプット、アウトプットのパターンが多く、アサーションを記述するテストはコストが高い場面

例えば下記のような利用方法です。

  • テスト整備が不十分だが、現状の動きは正しいとみなせるモジュールに対して、スナップショットテストを低コストで整備してリファクタリングをしていく
  • アプリケーションとして重要なモジュールに対して、念入りな手動テストに加えてリグレッションテストとしてスナップショットテストを整備する

テストピラミッドでいうと結合テストのレイヤーで他のテストと組み合わせて使いやすいです。

逆に下記のような場合はテストの目的を果たしにくいです。

  • 現状のアプリケーションのそもそもの動作が妥当であることをテストする
  • テスト駆動開発のようにテストテストファーストで書きたい

活用例

ログラスのプロダクトである経営管理クラウドLoglassにはお客様のデータを集計、数値計算を行うエンドポイントがあり、その結合テスト観点でのリグレッションテストとしてスナップショットテストが利用されています。
Loglassにおいて数値情報はユーザの経営の意思決定を左右する重要情報なため、このAPIに関わりそうな箇所を改修、リファクタリングする際はデグレしていないか細心の注意が必要となります。
内部のモジュールに対して単体テストが整備されていますが、ロジックは複雑かつ多くのモジュールが関わるため、一部を変更した際に全体としてデグレを起こしてしまう可能性はあり得ます。
結合テストとして手動でのテストも実施されており、現時点で動作の妥当性は保証されていると言っていい状況ですが、モジュールの改修ごとに手動のテストを行うのはコストが高いという状況でした。
そのためこのエンドポイントに対して数百パターンのスナップショットテストを整備し、リグレッションテストとして運用しています。
テストの構築は1,2日ほどかかりましたが、機能の複雑さに対しての工数としてはかなり少なく、コスパの良いテストになっていると感じています。

その他下記のような場面のリグレッションテストとして利用できると考えています。

  • メールや通知の文面、エラーメッセージなどのテキスト
  • 複雑なロジックを持つSQLのviewの実行結果

まとめ

スナップショットテストは万能ではないですが、他のテストと組み合わせて使うことでコスパよくテスト構築ができるので、選択肢の一つとして持っておけるとよいと思います。
メジャーなテストツールではJestくらいにしか標準装備されておらず、ログラスでもKotlin(JUnit5)で利用するためにjava-snapshot-testingという拡張を利用していますが、もっと広がって欲しいです。

宣伝

ログラスは一緒にテクノロジーで経営をアップデートしていくメンバーを募集しています。アプリ開発を爆速にできるようなテスト設計を一緒にしていきたいです。ご興味ある方ぜひカジュアルにお声掛けください。
https://job.loglass.jp/#block-5e51009601504072960ac2710802a577

株式会社ログラス テックブログ

Discussion