Edited at

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

More than 3 years have passed since last update.


概要

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


基本

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


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でテストしてやれば良い。


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されるような実装である。


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


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


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



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で用意されているので、以下のように簡単にかける。


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を一切かませずにコンポーネントのテストがかけた。


参考