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

## 種類と特徴
見づらくてすみません:bow: なるべく広げて見てください。

|  | コンポーネントテスト | スナップショットテスト | メソッドテスト |
|:--|:--|:--|:--|
| ツール<br>の例 | enzyme<br>任意のテストランナー<br>任意のアサーションライブラリ | react-test-render<br>jestなどの対応ツール<br>　　　　　　　　　　　　　　 | 任意のテストランナー<br>任意のアサーションライブラリ |
| 手法 | jqueryライクにDOMを操作<br>その際の変化や挙動を確認 | 正常なDOMツリーを出力保存<br>コード修正後の出力と比較し<br>意図しない変更がないか確認 | メソッドを直接実行<br>挙動を確認 |
| メリット | 特定の動きのテストができる | 作成、メンテコストが低い | メソッドのみでテストできる |
| デメリット | ・テストコードが煩雑<br>・作成、メンテコストが高い | ・"正常な状態" が最初に必要<br>・エラー原因を見つけにくい | ・全てのメソッドを<br>　テストできる訳ではない |
| 可能な<br>テスト | イベント<br>propsによる表示の変化<br>ライフサイクルメソッド<br>動き系メソッド（結合）<br>表示系メソッド（結合）<br>　-  | 　- <br>propsによる表示の変化<br>　- <br>　- <br>表示系メソッド（結合）<br>　-  | 　- <br>　- <br>　- <br>　-　<br>　- <br>動き系メソッド（単体） |
| 向いて<br>いる役割 | コンポーネント内<br>結合テスト | デグレーションテスト | 不安（≒複雑）なメソッドの<br>重点テスト |

## enzymeの使い方
### renderの種類
- [mount](http://airbnb.io/enzyme/docs/api/mount.html)
  - 子コンポーネントまで展開する
  - 重い
  - 子供まで見ないといけない場合に限定して利用
- [shallow](http://airbnb.io/enzyme/docs/api/shallow.html)
  - 子コンポーネントは展開しない
  - 軽量
  - 通常はこっち

eg. shallowするときの記述

```javascript
const props = { hoge : 1, fuga : 2 };
const wrapper = shallow(<Component {...props} />);
```

### 動きのシミュレーション実施例
#### クリックする
チェックボックス、ラジオボタンの選択でも使える

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

#### テキストボックスに入力する
値が挙動に関係ないのであれば、simulateの第二引数は無くてもok

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

### テスト実施例集
#### 表示分岐のテスト

```javascript
// テスト対象
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も使います

```javascript
// テスト対象
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が変わった時のテスト
ライフサイクルメソッドのテストに使えます。

```javascript
// テスト対象
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」のテストは別で実施した方が
コケた時に原因がわかりやすいです。

<br><br><br>


## スナップショットテストの仕方
[jestの公式ドキュメント](https://facebook.github.io/jest/docs/ja/snapshot-testing.html)にあるままですが、さくっと紹介します。

### 1. スナップショットファイルを作成、保存
下記の記述をテストコードに書き、jestを実行すると、
テストファイルの隣に　\_\_snapshots__ ディレクトリが掘られて、
そこにスナップショットファイルが作られます。
こちらのスナップショットファイルもコミットしておきましょう。

```javascript
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();
});
```
```bash
$(npm bin)/jest
```

### 2. 編集した結果、差分が出るとテストがこける
```diff
Received value does not match stored snapshot 1.
    
- Snapshot
+ Received
    
  <a
    className="normal"
-   href="#"
+   href="#a"
    onMouseEnter={[Function]}
    onMouseLeave={[Function]}
```

### 3. 意図通りの差分であれば、スナップショットを更新
スナップショットの差分もコミットすることになるので、PRで一緒にチェックができます。

```bash
$(npm bin)/jest --updateSnapshot
```

<br><br><br>


## メソッドテストの仕方
### staticなメソッド
```javascript
// テスト対象
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を参照するメソッド
```javascript
// テスト対象
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のテスト手法の分類とそれぞれの実施例を紹介
- 適宜使い分けて、どんどんコンポーネントテストを書きましょう♪

## 参考資料
- [enzyme document](http://airbnb.io/enzyme/)
- [sinon.spy document](http://sinonjs.org/releases/v2.0.0/spies/)
- [jest document](https://facebook.github.io/jest/docs/ja/snapshot-testing.html)
- [Testing component methods](https://github.com/airbnb/enzyme/issues/208)