ReactのテストにJest Snapshotが便利

かなり久しぶりの投稿になってしまいました…。最近新しいお客さんの仕事でインプットが多めだったんですが、そろそろ落ち着いてきてるので忘れないうちに色々書き出しておきたいです。まずは最近お世話になっているJestについて。フロントエンドのテストの最適解っておそらくライブラリやフレームワークによってまちまちだとは思うんですが、ReactだとJestのSnapshot気に入って使ってます。

Jest Snapshotとは?

下記が公式になります。

Snapshot Testing

Snapshotを使うまでは、AirBnBのEnzymeを少し使ったことがある程度でした。Enzyme使いやすくて好きなんですが、如何せんテストを書くのに時間がかかるな〜と思っていました。たとえば、出力する文章をassertする場合でも、Reactって<!-- react-empty: 30 -->みたいなコメントが入ったりするのでコメントをstripしてやったり。あとセレクタ辿ってDOMを正しく指定するのも物量が多いとなかなかつらいものがあったりします。で、そのわりに表示確認系のテストはデザインなど変更される事もあるので時間がかかるわりにはあまり美味しくないという気もします。

Snapshotはそんな表示系のテストの課題を解決してくれるかもしれないアプローチをとっています。今まではどう表示されているかをコードで確認するというアプローチでしたが、Snapshotは表示をそのままSnapshotとしてまず保存しGitに含めて管理します。そしてテストの度に前回のSnapshotと差分をチェックし表示内容が変化してないかを確認します。ズレてればSnapshot TestでFailがあがります。差分がなければグリーン。

このアプローチだと表示確認に必要なコードは最小限になります。たとえば、下記のような感じです。ちなみにこちらにサンプルのコードがあります。

  it('snapshot1', () => {
    const component = renderer.create(
      <App greeting="Hello simple"/>
    );
    expect(component).toMatchSnapshot('Greeting Hello simple is displayed?');
  });

toMatchSnapshot()を呼び出すだけなので本当に一瞬です。これで下記のようなSnapshotがsnapshotsディレクトリに保存されます。

exports[`Greeting Hello simple is displayed? 1`] = `
<div
  className="container"
>
  <div
    className="jumbotron"
  >
    <h1>
      Hello simple
    </h1>
  </div>
</div>

ただ、注意点としては、テストを書く時間は短くなるのですが、コードで書いた場合と異なり下手するとそのSnapshotが何を確認したいものなのかとても曖昧になってしまう可能性があります。そのため、Snapshot名はなるべく具体的にどうなっていると正しいのかを必ず記述するのが良いと思います。toMatchSnapshotの引数でSnapshot名を指定できます。

Enzymeとの併用

Snapshotだけでかなりの表示系をカバーする事ができそうですが、ぼくの場合は下記のような場合にEnzymeを併用したくなりました。

  • フォーム画面でバリデーションエラーが適切に表示されているか確認したい場合
  • バリデーションに限らず何かしらのイベントで表示が変わる場合に適切に変わっているか確認したい場合

全てpropsでうまく切り替われば表示だけは確認できたりしますが、やはりイベントと絡めて確認したい状況は少なくないように思います。そのような場合にEnzymeを使うと便利です。で、このEnzymeを使っていてもSnapshotを取ることができます。下記を利用します。

jest-serializer-enzyme

以前は、enzyme-to-jsonというパッケージを利用していたんですが、jest-serializer-enzymeのほうがSerializerとして実装されているので、コードではなく設定で有効化することができ、またJestの流儀にのっているという意味では今後はこちらのほうが使われていくように思います。

ちなみにEnzymeとjest-serializer-enzymeをサンプルに入れて試した時のコミットはこちらです。jestの設定でSerializerを指定しています。ちなみにこの設定はpackage.jsonに書いても良いです(その場合は最初のキーはjestでその中に設定を書きます)。

こんな感じのコードになります。先ほどとほとんど変わらないですね。

  it('snapshot2', () => {
    const wrapper = mount(
      <App greeting="Hello enzyme"/>
    );
    expect(wrapper).toMatchSnapshot('Greeting Hello enzyme is displayed?');
  });

これでイベントをsimulateしてから確認したい場合などは下記のようなコードを入れてからSnapshotを取る形になります。

wrapper.find('input[name="email"]').simulate('blur'); // emailのinputでフォーカスアウト
wrapper.find('input[name="email"]').simulate('change', {target: {value: 'test@example.com'}}); // inputに値を入れる

表示の確認のコードがないのでだいぶテストの実装はすっきりすると思います。

HTMLとしてSnapshotを確認したい

ちなみに、Snapshotはとても便利なんですが、Snapshot自体はただのマークアップなので、確認する時にマークアップ全体を見渡す形になるのでどうしても確認が雑になりやすいです。ぼくはその度に.snapの拡張子を.htmlに変えてブラウザで確認したりしてました。ただ、そんな事しなくても素敵なSnapshot Viewerとも呼ぶべきパッケージが提供されています。

jest-html

こちらがjest-htmlを加えた時のコミットです。基本的には先程のjest-serializer-enzymeと同じくSerializerを入れる感じになります。Serializerは複数指定してOKのようです。

注意点としては、jest-htmlをSerializerに加えてからテストをするとSnapshotの形が少し変わるので、Snapshotの更新が必要になります。その場合は、yarn test -- -uのようにして-uオプションを渡してSnapshotを現在の結果で上書きします。

Snapshotをjest-html版に書き換えたら、jest-htmlコマンドを実行すると下記のようなViewerが起動し、各SnapshotをHTMLとして表示確認することができます。これは素晴らしいです。

02

サンプルのほうで、ここまで確認できるようになっていますので、もし興味を持たれたらぜひお試しください。簡単に試すことができると思います。

実はバックエンドでも使えるSnapshot

ちなみにですが、SnapshotのテストはJSONを返すAPIのテストなども同じように使うことができるようです。この発想はぼくにはなかったんですが同じチームの方が試されてました。テストにもよると思いますが、確かにAPIの応答のテストなどでは使えそうです。ただ、雑にSnapshot取るだけになってしまうと特にバックエンドだと「どういう状態が正しいのか」が分かりにくくなってしまうかもしれません。そこに留意して工夫できてると良いかも。

JestのSnapshotは2016年の夏頃リリースされた機能なんですが、今どれくらい使われているんでしょうか?個人的にはとても気に入っていて、今後いろんな応用ができるようになっていくといいなと思っています。