Jestを使ってReactのコンポーネントをスナップショットテストしてみる
セットアップ いつも通りディレクトリ作成とnpmのセットアップをやります。 $ mkdir try-jest && cd try-jest $ npm init -y Reactインストール なにはともあ […]
目次
セットアップ
いつも通りディレクトリ作成とnpmのセットアップをやります。
$ mkdir try-jest && cd try-jest
$ npm init -y
Reactインストール
なにはともあれReactまわりのコンポーネントを入れましょう。
$ npm i -S react react-dom
Babelセットアップ
これもいつも通りですが、Babelでトランスパイルできるようにしておきます。
パッケージインストール
$ npm i -D babel-preset-es2015 babel-preset-react
.babelrc
{
  "presets": ["es2015", "react"]
}
テスト用コンポーネントを作る
Jestを入れる前に、ひとまずコンポーネントを作ってみましょう。
テストを動かしてみることが目標なので、簡単なコンポーネントを用意しましょう。
今回はボタンのタイプとテキストを指定できるコンポーネントを作ってみました。
ExampleBtn.js
import React from 'react'
export default class ExampleBtn extends React.Component {
  render () {
    return (
      <button
        type={this.props.type}
      >
        {this.props.children}
      </button>
    )
  }
}
ここからはこのコンポーネントをテストしていきます。
Jestセットアップ
それではいよいよJestをいれていきましょう。
Jest本体とBabel用のパッケージ、それにテストでReactコンポーネントをレンダーするためのパッケージの3つをいれます。
$ npm i -D babel-jest jest react-test-renderer
/node_modules/jest/bin/jest.jsが実行ファイルになりますので、実行してみて以下のように表示されるか確認してみましょう。
$ ./node_modules/jest/bin/jest.js  
No tests found
In /Users/EXAMPLE_USER/develop/try-jest
  5 files checked.
  testMatch: **/__tests__/**/*.js?(x),**/?(*.)(spec|test).js?(x) - 0 matches
  testPathIgnorePatterns: /node_modules/ - 5 matches
Pattern: "" - 0 matches
「__tests__というディレクトリにテストファイルがない」と言われています。
Jestのテストファイルはtestsディレクトリ配下に配置しますので、次は先ほど作ったコンポーネントのテストを書いてみましょう。
Jestでテストする
テストコードを書く
まずはフォルダとファイルを用意します。
$ mkdir -p __tests__/ExampleBtn
$ touch __tests__/ExampleBtn/tests.js
テストコードは以下のように書きます。
renderer.create()でコンポーネントを作成します。
__tests__/ExampleBtn/tests.js
import React from 'react'
import ExampleBtn from '../../ExampleBtn'
import renderer from 'react-test-renderer'
test('render test', () => {
  const component = renderer.create(
    <ExampleBtn type="text">Test</ExampleBtn>
  )
  let tree = component.toJSON()
  expect(tree).toMatchSnapshot()
})
テストを実行する
テスト対象とテストファイルが揃ったのでテストを実行しましょう。
$ ./node_modules/jest/bin/jest.js 
 PASS  __tests__/ExampleBtn/tests.js
  ✓ render test (12ms)
Snapshot Summary
 › 1 snapshot written in 1 test suite.
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 added, 1 total
Time:        0.771s, estimated 1s
Ran all test suites.
スナップショットテストとは?
ここまでで実行したテストは「スナップショットテスト」とよばれるものです。
Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly.
A typical snapshot test case for a mobile app renders a UI component, takes a screenshot, then compares it to a reference image stored alongside the test. The test will fail if the two images do not match: either the change is unexpected, or the screenshot needs to be updated to the new version of the UI component.*translate by google
スナップショットテストは、あなたのUIが予期せず変更されないようにするために非常に便利なツールです。
モバイルアプリケーションの典型的なスナップショットテストケースは、UIコンポーネントをレンダリングし、スクリーンショットを撮ってから、テストと一緒に保存された参照イメージと比較します。 2つのイメージが一致しない場合、テストは失敗します。変更が予期しない場合、またはスクリーンショットを新しいバージョンのUIコンポーネントに更新する必要がある場合。via:https://facebook.github.io/jest/docs/snapshot-testing.html
.toMatchSnapshot()を実行する度に、__snapshots_/test.js.snapにスナップショットが保存され、テスト実行時に比較が行われます。
__tests__/ExampleBtn/__snapshots__/test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render test 1`] = `
<button
  type="text"
>
  Test
</button>
`;
スナップショットテストをFailさせてみる
せっかくなので、このテストを失敗させてみましょう。
buttonタグのテキストを「Test | {指定した文字}」という表示になるように変更してみました。
import React from 'react'
export default class ExampleBtn extends React.Component {
  render () {
    return (
      <button
        type={this.props.type}
      >
        Hoge |
        {this.props.children}
      </button>
    )
  }
}
スナップショットに記録された出力内容と異なる出力になるので、これでテストがコケるはずです。
$ ./node_modules/jest/bin/jest.js 
 FAIL  __tests__/ExampleBtn/test.js
  ● render test
    expect(value).toMatchSnapshot()
    Received value does not match stored snapshot 1.
    - Snapshot
    + Received
     <button
       type="text"
     >
    +  Hoge |
       Test
     </button>
      at Object.<anonymous> (__tests__/ExampleBtn/test.js:10:16)
      at process._tickCallback (internal/process/next_tick.js:103:7)
  ✕ render test (28ms)
Snapshot Summary
 › 1 snapshot test failed in 1 test suite. Inspect your code changes or re-run with `-u` to update them.
Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   1 failed, 1 total
Time:        1.238s
Ran all test suites.
はい。見事にこけました。
このテストを入れておくことで、「コンポーネントの出力内容がいつのまにか変わっている」という事故を防ぐことができそうですね。
「コンポーネントの変更したいんだけど・・・」という場合
コンポーネントの出力が前回テスト時のものと変わっているとコケるのがスナップショットテスト(という理解であっているはず)です。
とするとコンポーネントの変更をやりたい場合はどうすれば良いのでしょうか。
先ほどのFailしたテスト結果をよくみると、その方法が紹介されています。
Inspect your code changes or re-run with
-uto update them.
(Google翻訳): コードの変更を調べるか、-uで再実行してコードを更新してください。
はい。-uオプションをつけて再実行すると良いみたいです。
$ ./node_modules/jest/bin/jest.js  -u
 PASS  __tests__/ExampleBtn/test.js
  ✓ render test (17ms)
Snapshot Summary
 › 1 snapshot updated in 1 test suite.
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 updated, 1 total
Time:        0.944s, estimated 1s
Ran all test suites.
ということで再実行した結果、テストがPassしました。
スナップショットを確認すると、先ほどのスナップショットが削除されて新しいものに置き換わっています。
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render test 1`] = `
<button
  type="text"
>
  Hoge |
  Test
</button>
`;
所感
Fail時のメッセージを読む限り、スナップショットテストはあくまで「予期せぬコンポーネント出力の変更を検知するためのテスト」という位置づけっぽいです。
DOMテストの方法もドキュメントに記載されていますので、Jestでのテストではこのあたりを組み合わせて使うことになるのかなと思います。