Edited at

ReactComponentのテストについてまとめ

More than 1 year has passed since last update.


種類と特徴

見づらくてすみません:bow: なるべく広げて見てください。

コンポーネントテスト
スナップショットテスト
メソッドテスト

ツール
の例
enzyme
任意のテストランナー
任意のアサーションライブラリ
react-test-render
jestなどの対応ツール
              
任意のテストランナー
任意のアサーションライブラリ

手法
jqueryライクにDOMを操作
その際の変化や挙動を確認
正常なDOMツリーを出力保存
コード修正後の出力と比較し
意図しない変更がないか確認
メソッドを直接実行
挙動を確認

メリット
特定の動きのテストができる
作成、メンテコストが低い
メソッドのみでテストできる

デメリット
・テストコードが煩雑
・作成、メンテコストが高い
・"正常な状態" が最初に必要
・エラー原因を見つけにくい
・全てのメソッドを
 テストできる訳ではない

可能な
テスト
イベント
propsによる表示の変化
ライフサイクルメソッド
動き系メソッド(結合)
表示系メソッド(結合)
 -
 -
propsによる表示の変化
 -
 -
表示系メソッド(結合)
 -
 -
 -
 -
 - 
 -
動き系メソッド(単体)

向いて
いる役割
コンポーネント内
結合テスト
デグレーションテスト
不安(≒複雑)なメソッドの
重点テスト


enzymeの使い方


renderの種類



  • mount


    • 子コンポーネントまで展開する

    • 重い

    • 子供まで見ないといけない場合に限定して利用




  • shallow


    • 子コンポーネントは展開しない

    • 軽量

    • 通常はこっち



eg. shallowするときの記述

const props = { hoge : 1, fuga : 2 };

const wrapper = shallow(<Component {...props} />);


動きのシミュレーション実施例


クリックする

チェックボックス、ラジオボタンの選択でも使える

wrapper.find('[data-test=input]').simulate('click');


テキストボックスに入力する

値が挙動に関係ないのであれば、simulateの第二引数は無くてもok

wrapper.find('[data-test=input]').simulate('change', { target : { value : input }});


テスト実施例集


表示分岐のテスト

// テスト対象

const Component = (props) => (
<div>
{props.isHoge
? <span date-test="hoge">ほげ</span>
: <span date-test="fuga">ふが</span>
}
</div>

);

// 確認したいprops状態にして、shallowレンダリング
const props = { isHoge : true };
const wrapper = shallow(<Component {...props} />

// find + exists を使って、意図した表示になっているか確認
expect([
wrapper.find('[data-test=hoge]').exists(),
wrapper.find('[data-test=fuga]').exists(),
]).toEqual([
true,
false,
]);


クリックイベントのテスト

sinonも使います

// テスト対象

const Component = (props) => (
<div>
<button
data-test="btn"
onClick={() => props.hogeAction())
>
ボタンだよ
</button>
</div>
);

// テスト用props actionが実行されたかを見るため、sinon.spyを仕込む
const props = {
hogeAction : sinon.spy(),
};

// shallowレンダリング
const wrapper = shallow(<Component {...props} />);

// "ボタンを押す" 動きをシミュレートする
wrapper.find('[data-test=btn]').simulate('click');

// sinon.spyを通して、意図したアクションが呼ばれているか確認
expect(props.hogeAction.calledOnce).toEqual(true);


propsが変わった時のテスト

ライフサイクルメソッドのテストに使えます。

// テスト対象

class Component extends React.Component {
componentWillReceiveProps(nextProps) {
// false->trueに変わった時だけgetが呼ばれるので、この挙動をテストする
if (this.props.isHoge === false && nextProps.isHoge === true) {
this.props.getHoge();
}
}
// 他のメソッドは省略
}

// テスト用props 実行されたかを見るため sinon.spy を仕込む
const props = {
isHoge : false,
getHoge : sinon.spy(),
};

// shallowレンダリング
const wrapper = shallow(<Component {...props} />);

// propsを変更
wrapper.setProps({
...props,
isHoge : true,
});

// sinon.spyを通して呼ばれたことを確認
expect(props.getHoge.calledOnce).toEqual(true);

// propsを変更
wrapper.setProps({
...props,
isHoge : false,
});

// sinon.spyを通して呼ばれていないことを確認
expect(props.getHoge.calledTwice).toEqual(false);

※ 説明用にまとめて書いていますが、実際はbeforeEachなどを使って、

「false -> true」と「true -> false」のテストは別で実施した方が

コケた時に原因がわかりやすいです。





スナップショットテストの仕方

jestの公式ドキュメントにあるままですが、さくっと紹介します。


1. スナップショットファイルを作成、保存

下記の記述をテストコードに書き、jestを実行すると、

テストファイルの隣に __snapshots__ ディレクトリが掘られて、

そこにスナップショットファイルが作られます。

こちらのスナップショットファイルもコミットしておきましょう。

import React from 'react';

import Link from '../Link.react';
import renderer from 'react-test-renderer';

it('Linkの正常系のスナップショット', () => {
const tree = renderer
.create(<Link page="http://www.instagram.com">Instagram</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});

$(npm bin)/jest


2. 編集した結果、差分が出るとテストがこける

Received value does not match stored snapshot 1.


- Snapshot
+ Received

<a
className="normal"
- href="#"
+ href="#a"
onMouseEnter={[Function]}
onMouseLeave={[Function]}


3. 意図通りの差分であれば、スナップショットを更新

スナップショットの差分もコミットすることになるので、PRで一緒にチェックができます。

$(npm bin)/jest --updateSnapshot





メソッドテストの仕方


staticなメソッド

// テスト対象

class Component extends React.Component {
static isHoge(flag1, flag2) {
return (flag1 === false || flag2 === true);
}
// 他のメソッドは省略
}

// テストパターンを定義
const dataProvider = {
'flag1=true, flag2=true なら true' : {
flag1 : true,
flag2 : true,
expected : true,
},
'flag1=true, flag2=false なら false' : {
flag1 : true,
flag2 : false,
expected : false,
},
// パターンは省略
};

//ループでテスト
Object.entries(dataProvider).forEach((testCase, desc) => {
test(desc, () => {
expect(
// スタティックなのでそのまま呼び出せる
Component.isHoge(testCase.flag1, testCase.flag2)
).toEqual(
testCase.expected
);
});
});


props, stateを参照するメソッド

// テスト対象

class Component extends React.Component {
isHoge() {
return (this.props.flag1 === false || this.props.flag2 === true);
}
// 他のメソッドは省略
}

// テストパターンを定義
const dataProvider = {
'flag1=true, flag2=true なら true' : {
props : {
flag1 : true,
flag2 : true,
},
expected : true,
},
'flag1=true, flag2=false なら false' : {
props : {
flag1 : true,
flag2 : false,
},
expected : false,
},
// パターンは省略
};

//ループでテスト
Object.entries(dataProvider).forEach(([desc, testCase]) => {
test(desc, () => {
// enzymeの.instanceを使う
const instance = shallow(<Compont {...testCase.props} />).insetance();
expect(
instance.isHoge()
).toEqual(
testCase.expected
);
});
});


stateを変更するメソッド

単体ではやり方がわからなかったです。(多分なさそう)

enzymeでレンダリングした上で、setStateなどを駆使して実施してください。


まとめ


  • ReactComponentのテスト手法の分類とそれぞれの実施例を紹介

  • 適宜使い分けて、どんどんコンポーネントテストを書きましょう♪


参考資料