My External Storage

Sep 28, 2018 - 6 minute read - Comments - react test

Jest( >23.0.0 )、enzymeでReactのテーブル駆動テストを行う #react #test

Reactでもテーブル駆動テスト(データ駆動テスト)がしたいと思い、Jestを使ってみた。 ステートレスなコンポーネントがちゃんと設計できていれば入出力は冪等になるので、Reactとテーブル駆動テストは非常に相性がよさそう。 enzymeを使えばDOMアクセスも簡単だった。 ただ、Jestのバージョンが23.0.0以上じゃないとeachメソッドが使えないので、create-react-appで作ったプロジェクトの場合はejectする必要があった。

JestはJavascriptでrspecのようなテストが書けるツール。enzymeはairbnbが作成したいい感じにDOMにアクセスできるAPIライブラリ。

TL;DR

  • create-react-appで作ったプロジェクトの場合はejectしてJestをアップグレードする
  • enzymeyarn addしておくとテストがラク
  • setupTest.jsを足してテストを書けば終わり
  • yarn test -watchでホットリロードしながらテストができる
  • node --inspect-brk node_modules/.bin/jest --runInBandを使えばChromeコンソールでいろいろ試せる

サンプルリポジトリは以下。

import React from 'react';
import { shallow  } from 'enzyme';

import App from './App';

const emptyProps ={
  text: 'test'
};

describe('App', () => {
  describe('App-title', () => {
    const cases = [
      ['simple test1', { text: 'Jest'}, 'Welcome to Jest'],
      ['simple test2', { text: 'enzyme'}, 'Welcome to enzyme']
    ];
    describe.each(cases)('change Props', (title, override, expected) => {
       it(title, () => {
         const customProps = Object.assign({}, emptyProps, override);
         const wrapper = shallow(<App {...customProps} />);
         expect(wrapper.instance().props.text).toBe(customProps.text);
         expect(wrapper.find('.App-title').text()).toBe(expected);
       });
    });
  });
});

利用した各々のバージョンは次のとおり。

$ create-react-app --version
1.5.2

$ node -v
v10.8.0

$ yarn -v
1.9.4

$ egrep "react|jest|enzyme" package.json
"jest": "23.6.0",
"react": "^16.5.2",
"react-dom": "^16.5.2",
"enzyme": "^3.6.0",
"enzyme-adapter-react-16": "^1.5.0",
"react-test-renderer": "^16.5.2"
....

プロジェクトの準備

まず、テーブル駆動テストを行うReactプロジェクトを作成する。

$ create-react-app tdt-react-jest-enzyme

テーブル駆動テストを利用しない場合は、このままenzymeをインストールすればよい。
Jestについてはcreate-react-appコマンドでプロジェクトを作成した時点でインストールされている。
が、create-react-appの最新版(1.5.2)でも、プリインストールされているJestのバージョンは20.X系だった(ejectするとわかる)。

"jest": "20.0.4",

Jestでテーブル駆動をするために必要なdescribe.eachは23.X系からなので利用できない。


2018/10/04追記
create-react-appの2.X系がリリースされ、デフォルトのJestも23.X系になった。
これからcreate-react-appコマンドで新規に作るプロジェクトはyarn ejectしてJestをアップデートする必要はない。
ローカルにあるcreate-react-appコマンドはnpm install -g create-react-appで最新版に更新出来る。


そのため、yarn ejectcreate-react-appプロジェクトを解体したあと、Jestをアップグレードする。

$ yarn eject
$ yarn upgrade jest --latest
$ yarn add -D enzyme enzyme-adapter-react-16 react-test-renderer

enzymecreate-react-appのテンプレートに書いてあるとおりにインストールすれば良い。

$ yarn add -D enzyme enzyme-adapter-react-16 react-test-renderer

あとはenzymeを読み込むsetupTests.jsを作成する。

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

Jestが読み込むようにpackage.json"setupTestFrameworkScriptFileという設定を追加する。

  "jest": {
   ...
      "mjs"
    ],
    "setupTestFrameworkScriptFile": "<rootDir>/src/setupTests.js"
  },

これでプロジェクトでテーブル駆動テストができる状態にできた。
あとは以下の命名規則に則ってテストファイルを作成してyarn testを実行すればテストが始まる。

Jestでテーブル駆動テストを書く

まず、テスト対象のコンポーネントを作成する。今回はcreate-react-appで出来たsrc/App.jsを少し改良した。

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to { this.props.text }</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

App.propTypes = {
  text: PropTypes.string
};

App.defaultProps = {
  text: 'React'
};

export default App;

無修正だと本当にImmutableなため、Propsをもたせて文字列が変わるようにしてある。
テストは以下の通り。__tests__ディレクトリに入れるほうが好きだが、今回はsrc/App.test.jsファイルがあるのでそれを修正した。

import React from 'react';
import { shallow  } from 'enzyme';

import App from './App';

const emptyProps ={
  text: 'test'
};

describe('App', () => {
  describe('App-title', () => {
    const cases = [
      ['simple test1', { text: 'Jest'}, 'Welcome to Jest'],
      ['simple test2', { text: 'enzyme'}, 'Welcome to enzyme']
    ];
    describe.each(cases)('change Props', (title, override, expected) => {
       it(title, () => {
         const customProps = Object.assign({}, emptyProps, override);
         const wrapper = shallow(<App {...customProps} />);
         expect(wrapper.instance().props.text).toBe(customProps.text);
         expect(wrapper.find('.App-title').text()).toBe(expected);
       });
    });
  });
});

Rubyを書いているひとならば、rspecと文法が似ているのでは読みやすいのではないだろうか?
casesで作成したテストデータのテーブルの内容の数だけ、3回目のdescribe内が実行される。
これをyarn testで実行してみると、テストが2ケース実行されているのがわかる。

 PASS  src/App.test.js
  App
    App-title
      change Props
        ✓ simple test1 (5ms)
        ✓ simple test2 (1ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.888s, estimated 1s
Ran all test suites.

Jestはyarn test --watchコマンドで実行するとファイル更新を検知して自動でテストを再実行してくれる。
これでテーブル駆動テストをしながらテスト駆動開発(TDD)をすることができる。

テストを書くときのTips

enzymeはDOMコンポーネントにいろいろなアクセスをすることができる。メソッドも実行することができる。
APIリファレンスはサンプルコードも多いので、一通り眺めるとだいたいやりかたが載っている。
Matcherの書き方も、JestのAPIリファレンスの情報量が多いので公式を見れば良い。

イマイチテストがうまく動かない、あるいは目的のデータにどうアクセスしたらいいのかわからないときは、 以下のコマンドでデバッガを起動してREPL的に検証できる。

$ node --inspect-brk node_modules/.bin/jest --runInBand

終わりに

普段はGoを書いているので、Reactでもテーブル駆動テスト(TDT)が書きたかった。
実際、Jestは--watchでホットリロードテストが書けるのでTDT+TDDでかなり実装が捗っている。
また、今回は省略したが、Flowとの併用も可能だ(おそらくTypeScriptととも)。
Reduxなどに対するテストはまだ書いたことがないので、いずれやってみる。

参考

関連記事