React.js + Jest


前回はTestUtilsの使い方を中心に説明したので、今回はfacebookが開発しているJestというフレームワークとの組み合わせてみたいと思います。

https://facebook.github.io/jest/

## Painless JavaScript Unit Testing

Jestのページには「Painless JavaScript Unit Testing」とある通り導入が簡単という特徴を持っています。

その特徴として「**Mock By Default**」があって、**DefaultでCommonJS Styleの`require`を全てMockに置き換えます**。ちょっと過激な感じですね。

なので、テスト対象の挙動だけに依存したテスト簡単に書くことが出来ます。逆に完全にテスト対象以外はMockになるのでI/FのテストにはならないですがまぁそれはUnit Testの範囲外ということで。


## Jasmine

JestはJasmineをベースとしてその上に作られているので基本的なAssertなどはJasmineと同じです。
ただ、Jasmine 1.3をベースとしているので2.0で非同期のテストが書きやすくなった恩恵は受けることができません。

↓それに関するissue

https://github.com/facebook/jest/issues/74


## DOM

JestはjsdomによるDOMの上で実行されるので、nodeでのテストのようにcommand line I/Fでテストを実行することが出来ます。

つまり`Jest`を使っておけばKarmaのようなTest Runnerも使う必要がありません。簡単に導入できます。


## Install

Installは`jest-cli`をインストールします。

```
% npm install --save-dev jest-cli
```


## __tests__

defaultの設定だと、`__tests__`というディレクトリを探してきて、その中のファイルをテストとして実行します。

なのでGetting Startedにある通り、`__tests__`ディレクトリを作って、その中にテストを置いて`jest`を実行するだけでOKです。
globalではなくてdevDependenciesにインストールする場合、package.jsonのscriptsに下記のように書いて`npm test`で実行すると便利です。

```json
  "scripts": {
    "test": "jest"
  },
```


## React.jsのテストをする

https://facebook.github.io/jest/docs/tutorial-react.html

ちゃんとDocumentにReact.jsを使ったアプリケーションをテストする場合についても書かれています。
2つの設定をする必要があります。


### JSXの変換

JSXを使ってアプリケーションを書いている場合、テストの中でもJSXの変換が必要になります。
そこでjestでは`scriptPreprocessor`としてpackage.jsonにpreprocessorのscriptを指定することが出来るので、そこでJSXの変換を行います。

これにはreactではなく、`react-tools`が必要になるのでinstallしておく必要があります。

```package.json
  "jest": {
    "scriptPreprocessor": "preprocessor.js"
  },
```

```preprocessor.js
var ReactTools = require('react-tools');
module.exports = {
  process: function(src) {
    return ReactTools.transform(src, {harmony: true});
  }
};
```

これだけです。


### Mockの解除

上でも書いたようにJestでは全てのrequireがMockを返すようになります。
ただ、React自体をMockされるとテストにならないのでreactへのpathだった場合はMockしないように設定する必要があります。

それもpackage.jsonに指定するだけでOKです。
テストファイルにMockしないファイルを指定出来るのですが、全てのテストでMockしたくない場合はここに書いておくと便利です。

```package.json
  "jest": {
    "scriptPreprocessor": "preprocessor.js",
    "unmockedModulePathPatterns": ["node_modules/react"]
  },
```

## テストを書いてみる

実際にReact Componentのテストを書くとこんな感じになります。

```js
jest.dontMock('../InputArtist');

var React = require('react/addons'),
    InputArtist = require('../InputArtist'),
    AppTracksActionCreators = require('../../actions/AppTracksActionCreators')
;

describe("inputArtist", function() {
  var inputArtist;
  beforeEach(function() {
    inputArtist = React.addons.TestUtils.renderIntoDocument(<InputArtist />);
  });

  describe("state",  function() {
    it("set inputArtist radiohead", function() {
      expect(inputArtist.state.inputArtist).toBe("radiohead");
    });
  });

  describe("handleSubmit", function() {
    var preventDefault;
    beforeEach(function() {
      preventDefault = jest.genMockFunction();
      inputArtist.setState({ inputArtist: 'travis' });
      React.addons.TestUtils.Simulate.submit(inputArtist.getDOMNode(), {
        preventDefault: preventDefault
      });
    });
    it ("calls AppTracksActionCreators.fetchByArtist with state.inputArtist", function() {
      expect(AppTracksActionCreators.fetchByArtist).toBeCalled();
      expect(AppTracksActionCreators.fetchByArtist).toBeCalledWith('travis');
    });
    it ("calls e.preventDefault", function() {
      expect(preventDefault).toBeCalled();
    });

  });
});
```

https://github.com/koba04/react-boilerplate/blob/master/app/components/__tests__/InputArtistTest.js

![jest test](http://i.gyazo.com/99d5518396a763dd7bd8e66eb22f0edd.gif)

では、詳細を見ていきます。

```js
jest.dontMock('../InputArtist');
```

MockしたくないmoduleはdontMockで明示的に指定します。

```js
var React = require('react/addons'),
    InputArtist = require('../InputArtist'),
    AppTracksActionCreators = require('../../actions/AppTracksActionCreators')
;
```

Reactはpackage.jsonのunmockedModulePathPatternsの指定にマッチしてMockされないようになっていますが、その他のmoduleはMock化されています。

```js
describe("inputArtist", function() {
  var inputArtist;
  beforeEach(function() {
    inputArtist = React.addons.TestUtils.renderIntoDocument(<InputArtist />);
  });

  describe("state",  function() {
    it("set inputArtist radiohead", function() {
      expect(inputArtist.state.inputArtist).toBe("radiohead");
    });
  });
```

この辺りは普通のJasmineのテストコードですね。
`React.addons.TestUtils.renderIntoDocument`を使うことでComponentをDOMに紐付けてそれを使ってテストを書いています。

```js
  describe("handleSubmit", function() {
    var preventDefault;
    beforeEach(function() {
      preventDefault = jest.genMockFunction();
      inputArtist.setState({ inputArtist: 'travis' });
      React.addons.TestUtils.Simulate.submit(inputArtist.getDOMNode(), {
        preventDefault: preventDefault
      });
    });
    it ("calls AppTracksActionCreators.fetchByArtist with state.inputArtist", function() {
      expect(AppTracksActionCreators.fetchByArtist).toBeCalled();
      expect(AppTracksActionCreators.fetchByArtist).toBeCalledWith('travis');
    });
    it ("calls e.preventDefault", function() {
      expect(preventDefault).toBeCalled();
    });
```

ここではsubmit buttonが押された場合に`fetchByArtist`と`e.preventDefault`が呼ばれるかどうかをテストしています。
`React.addons.TestUtils.Simulate.submit`を使ってsubmitイベントを発行してイベントオブジェクトのpreventDefaultを`jest.getMockFunction`によるMockにすることで呼ばれたことを確認しています。

fetchByArtistは実際だとajaxリクエストが投げられるのですが、JestがMockしてくれているのでとくに意識することなくテストを書くことが出来て簡単ですね。


## Mock

Mockは`jest.genMockFunction`などのAPIで自分で作ることも出来て、`mock` propertyに`calls`や`instances`などの呼び出した情報が記録されていくのでテストではそれを確認します。

また、Mock Functionに対して`mockReturnValue`を呼ぶことでMockしながらも指定した値を返すようにしたりすることも可能ですし、`mockImplementation`メソッドにcallbackを渡すことで、Mockの実装をすることも出来ます。


### Mock Assert

またassertとしてMockを確認するためのものも用意されていて、`expect(mockFunc).toBeCalled`のように
することも可能です。


### moduleを差し替える

モジュールの実装をテスト時に常に差し替えておきたい場合は、`__mocks__`を作りその中にmoduleの実装を置いておくことで可能です。

↓はsuperagentをMockしようとするとエラーになってしまうというissueがあり、それのworkaroundとしてMockを置いています。

https://github.com/koba04/react-boilerplate/blob/master/app/actions/__mocks__/superagent.js


## Timer

またsetTimerやsetIntervalを使っているような実装に対するテストの場合、`jest.runAllTimers`や` jest.runOnlyPendingTimers`を使うことで同期的にテストを書くことが出来ます。

`runAllTimers`は全てのTimerで待っている処理を実行させて、`runOnlyPendingTimers`はその時点でPendingになっているものだけを実行します。

setTimeoutで再帰しているような実装の場合、`runAllTimers`を使ってしまうと無限ループになるのでその時は`runOnlyPendingTimers`を使って1つずつ進めながらテストを書いていきます。



## API

APIは↓のページにまとまっているので、それぞれは紹介しないですが色々揃ってるのはわかるかと思います。

https://facebook.github.io/jest/docs/api.html#content



## ちょっとツラいところ

設定で少しはなんとか出来そうですが、Karmaなんかと比べるとテストの実行が遅いのがツラいかもです。
Issueもあるので改善されることを期待しています。

https://github.com/facebook/jest/issues/116


---------

というわけで今回はJestでのテストについて書きました。
明日はFluxについて書きたいと思います。