LoginSignup
62
61

More than 5 years have passed since last update.

前回はTestUtilsの使い方を中心に説明したので、今回はfacebookが開発している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

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で実行すると便利です。

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

React.jsのテストをする

ちゃんと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のテストを書くとこんな感じになります。

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();
    });

  });
});

jest test

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

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

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

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

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

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に紐付けてそれを使ってテストを書いています。

  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が押された場合にfetchByArtiste.preventDefaultが呼ばれるかどうかをテストしています。
React.addons.TestUtils.Simulate.submitを使ってsubmitイベントを発行してイベントオブジェクトのpreventDefaultをjest.getMockFunctionによるMockにすることで呼ばれたことを確認しています。

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

Mock

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

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

Mock Assert

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

moduleを差し替える

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

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

Timer

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

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

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

API

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

ちょっとツラいところ

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


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

62
61
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
62
61