React+ReduxのコンポーネントをKarma+Jasmineでユニットテストする


## **概要**

React+ReduxのコンポーネントをKarma+Jasmineでユニットテストする。

## **基本**

以下の様なカウンターのコンポーネントを想定する。

```js:counter.js
function increment() {
  return { type : COUNTER_INCREMENT };
}

export class Counter extends React.Component {
  static propTypes = {
    dispatch: React.PropTypes.func.isRequired,
    counter: React.PropTypes.number,
  }

  constructor() {
    super();
  }

  render() {
    return (
      <div className='container text-center'>
        <h1>Sample Counter</h1>
        <h2>{this.props.counter}</h2>
        <button onClick={this.props.dispatch(increment)}>
          Increment
        </button>
      </div>
    );
  }
}
```

これのテストは以下のようになる。仮想DOMを描画しデータ表示のテスト、また、sinon.jsで関数をspyし、実行されたかどうかのテストをしている。これはコンポーネントのテストなので、`関数の実行有無`までをテストし、関数自体は、actionやreducerでテストしてやれば良い。

* <a href="http://qiita.com/okmttdhr/items/decedca042e161980220" target="_blank">React+Reduxでasyncなactionをテストする - Qiita</a>
* <a href="http://qiita.com/okmttdhr/items/d5fcea3a54b4ad15e145" target="_blank">React+Reduxでreducerのテスト - Qiita</a>

```js:counter.spec.js
import React from 'react';
import TestUtils from 'react-addons-test-utils';
import { Counter } from './path/to/counter.js';

function renderWithProps (props = {}) {
  // 仮想DOMを描画する
  return TestUtils.renderIntoDocument(<Counter {...props} />);
}

describe('Counter', function () {
  let _rendered, _props, _spies;

  beforeEach(function () {
    _spies = {};

    // propsのモック
    _props = {
      // sinon.jsを使用し、関数をspyする
      dispatch: _spies.dispatch = sinon.spy(),
      counter: 0,
    };

    _rendered  = renderWithProps(_props);
  });

  // 単純なテキストの表示
  it('<h1>の描画', function () {
    const h1 = TestUtils.scryRenderedDOMComponentsWithTag(_rendered, 'h1');
    expect(h1.length).toEqual(1);
    expect(h1[0].textContent).toEqual('Sample Counter');
  });

  // propsが描画される
  it('<h2>の描画', function () {
    const h2 = TestUtils.scryRenderedDOMComponentsWithTag(_rendered, 'h2');
    expect(h1.length).toEqual(1);
    expect(h1[0].textContent).toEqual('0');
  });

  // コンポーネントのテストなので関数が実行されるかどうかまでをテストすればよい
  it('Incrementボタンをクリックでdispatchが走ること', function () {
    const btn = TestUtils.scryRenderedDOMComponentsWithTag(_rendered, 'button');
    TestUtils.Simulate.click(btn[0]);
    
    // spyされた関数が実行されたかどうかをチェックする
    expect(_spies.dispatch.called).toBe(true);
  });
});
```


## **propsの変化をテストする**

propsが変化したときのコンポーネントの振る舞いをテストしたいという時もある。

例えば、以下の様な場合を考える。counterが3以上になると、別のactionがdispatchされるような実装である。

```js:counter.js
function increment() {
  return { type : COUNTER_INCREMENT };
}

// 追加
function stopIncrement() {
  return { type : COUNTER_STOP_INCREMENT };
}

export class Counter extends React.Component {
  static propTypes = {
    dispatch: React.PropTypes.func.isRequired,
    counter: React.PropTypes.number,
  }

  constructor() {
    super();
  }

  // 追加
  componentWillReceiveProps(nextProps) {
    if (nextProps.counter > 2) {
      this.props.dispatch(stopIncrement);
    }
  }

  render() {
    return (
      <div>
        <h1>Sample Counter</h1>
        <h2>{this.props.counter}</h2>
        <button onClick={this.props.dispatch(increment)}>
          Increment
        </button>
      </div>
    );
  }
}
```

このテストは以下のように書く。

```js:counter.spec.js

// 略

import {callComponentWillReceiveProps} from './path/to/helper.js';

describe('Counter', function () {

  // 略

  it('counterが3以上でdispatch関数が実行される', function () {
    const nextProps = { ..._props, counter: 3 };
    callComponentWillReceiveProps(Counter, _props, nextProps);
    expect(_spies.dispatch.called).toBe(true);
  });  
});

```

```js:helper.js
import React from 'react';
import TestUtils from 'react-addons-test-utils';

/**
 * 引数に渡したコンポーネントの親の状態を変え、`componentWillReceiveProps`を発火させる。
 * `props`の変更による、描画結果のテストや、関数の発火などをテストする。
 *
 * @param {Object} Component
 * @param {Object} props
 * @param {Object} nextProps
 */
export function callComponentWillReceiveProps(Component, props = {}, nextProps = {}) {
  class ParentClass extends React.Component {
    constructor() {
      super();
      this.state = props;
    }
    render() {
      return <Component {...this.state} />;
    }
  }
  const parent = TestUtils.renderIntoDocument(<ParentClass />);
  parent.setState(nextProps);
}
```

まず、`props`を操るために、`ParentClass`という親コンポーネントを作成している。そして、引数として渡した`nextProps`を親クラスの`state`にセットし、それがCounterコンポーネントに`props`として伝播される。

これにより、`componentWillReceiveProps`が呼び出されるので、あとは`_spies.dispatch.called`をみてやればよい。



## **関数をstub**

コンポーネントでは、関数の中身をテストする必要はないので、余計な処理をしないように、関数をstubすると便利な時がある。stubはsinon.jsで用意されているので、以下のように簡単にかける。

```js:counter.spec.js
  // someMethodはCounterコンポーネントのメソッド

  it('stub', function () {
    sinon
      .stub(Counter.prototype, 'someMethod')
      .onCall(0)
      .returns(() => _spies.someMethod = sinon.spy());
    const someMethodKicker = TestUtils.scryRenderedDOMComponentsWithClass(_rendered, 'someMethodKicker');
    TestUtils.Simulate.click(someMethodKicker[0]);
    expect(_spies.someMethod.called).toBe(true);
  });
```


## **まとめ**

結果的に、actionやreducerを一切かませずにコンポーネントのテストがかけた。

## **参考**

* <a href="http://sinonjs.org/docs/#stubs" target="_blank">Sinon.JS - Documentation</a>
* <a href="https://facebook.github.io/react/docs/test-utils.html" target="_blank">Test Utilities | React</a>
* <a href="http://redux.js.org/docs/recipes/WritingTests.html" target="_blank">Writing Tests | Redux</a>