Reactコンポーネントのテストを書いてみた

# 最初に
Reactのテストの良い技法を紹介するものではありません。

私が手探りで、Reactコンポーネントのテストを書いた奮闘記です。

# 事の始まり
Reactのテスト書いてみたいな。

コンポーネントもクラスなのだから、他の言語と大してやることは変わらないはず！？
TDD風に実践したら、何か掴めるかもしれない。

# 実験用のコンポーネントを考える
私のReactコンポーネントのイメージは、

- render
- state(constructor)
- props
- イベントの関数

で構成されているイメージなので、それを全て内包しているものを考える。

ので、

- ボタン
- ボタンの活性|非活性が存在する(state)
- ボタンの表示名やクリック時の処理は外からもらう(props, 関数)

とかでどうじゃろか。

# 実験
## テストツールを入れる
別になんでもよかったのですが、Reactのテストということもあり、
Facebook製の**Jest**を使ってみることに。

```bash
npm install -D jest babel-jest babel-preset-env babel-preset-react react-test-renderer
```

```json:.babelrc
{
  "presets": ["env", "react"]
}
```

```js:jest.config.js
module.exports = {
    transform: {
        '^.+\\.js?$': 'babel-jest'
    }
};
```

scriptsも足しておきます。

```json:package.json
"scripts": {
    "jest": "jest"
},
```

## ボタンであることのテスト
enzymeとやらを使うとできそう。

https://facebook.github.io/jest/docs/ja/tutorial-react.html
http://airbnb.io/enzyme/docs/api/shallow.html

### enzymeの準備
Reactのバージョンに合わせたAdapterもいるらしい。

```bash:インストール
npm install -D enzyme enzyme-adapter-react-16
```

#### シャローレンダリング
第１階層の深さのコンポーネントだけレンダリングする。

enzymeの場合、
`shallow()`を用いると、シャローレンダリングされたShallowWrapperオブジェクトを取得できるようです。
このオブジェクトをテストとして使うようです。

### やってみよう
#### 手順
①テストしたいコンポーネントを、ShallowWrapperオブジェクトにする
②ShallowWrapperオブジェクトからノードを抽出する
③Jestを使ってテスト

#### 【その前に】ShallowWrapperでノードを抽出する話
ノードを抽出する関数は色々用意されているようですが・・・。

例を見る限り、

- CSSセレクタを指定した`find()`などでノードを抽出(これもまたShallowWrapper)
- 抽出したオブジェクトはlengthを持っている

ので、このlengthの数で存在確認を行えばいいわけですね。

#### 【その前に】Jestの基本的な使い方の話
[Jestの基本的な使い方](https://facebook.github.io/jest/docs/ja/expect.html)は、こんな感じ・・に見えます。

```js:Jest
describe('テストスィート', () => {
    test('テスト', () => {
        // expect <TestTarget> to be <ExpectedValue>
        expect(TestTarget).toBe(ExpectedValue);
    });
});
```

#### 書いてみよう

```js:src/my-button.test.js
import React from 'react';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import MyButton from './my-button';

Enzyme.configure({ adapter: new Adapter() });

describe('view', () => {
    test('button', () => {
        const wrapper = shallow(<MyButton />);  // shallowWrapper取得

        const buttonWrapper = wrapper.find('input[type="button"]');  // ボタン取得

        expect(buttonWrapper).toHaveLength(1);  // ボタンの存在確認
    });
});
```

で、テスト開始。

```bash:
$ npm run jest
FAIL  src/my-button.test.js
  ● Test suite failed to run

    Cannot find module './my-button' from 'my-button.test.js'

      2 | import Enzyme, { shallow } from 'enzyme';
      3 | import Adapter from 'enzyme-adapter-react-16';
    > 4 | import MyButton from './my-button';
        | ^
      5 | import renderer from 'react-test-renderer';
      6 | 
      7 | Enzyme.configure({ adapter: new Adapter() });

      at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:210:17)
      at Object.<anonymous> (src/my-button.test.js:4:1)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        1.165s, estimated 2s
```

失敗しました。ボタン用意していないですしね。
用意しましょう。

```jsx:js:src/my-button.js
import React, {Fragment} from 'react';

export default () => (
    <Fragment>
        <input type="button" />
    </Fragment>
);
```

もう一回テスト。

```bash:
$ npm run jest
 PASS  src/my-button.test.js
  view
    ✓ button (8ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.554s
```

通りましたヽ(*´∀`)/

## propsとテスト
次に簡単そうなのはpropsに関してですかね！？(思い込み

### 単純なpropsのテスト
ボタンの表示名をコンポーネントのプロパティとして設定します。

`ShallowWrapper.prop()`でプロパティの値が取れそうです。
使ってみましょう。

```js:src/my-button.test.js
import React from 'react';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import MyButton from './my-button';

Enzyme.configure({ adapter: new Adapter() });

describe('view', () => {
    test('button', () => {
        const value = "ボタンですよ";

        const wrapper = shallow(<MyButton value={value} />);
        const buttonWrapper = wrapper.find('input[type="button"]');

        expect(buttonWrapper).toHaveLength(1);
        expect(buttonWrapper.prop('value')).toBe(value);
    });
});
```

```jsx:src/my-button.js
import React, {Fragment} from 'react';

export default (props) => (
    <Fragment>
        <input type="button" value={props.value} />
    </Fragment>
);
```

```bash
$ npm run jest
 PASS  src/my-button.test.js
  view
    ✓ button (9ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.586s, estimated 2s
Ran all test suites.
```

### propsのバリデーション
PropTypesで引っかかったら、Errorオブジェクトを返して、
それを検知するようなテストをかければいいなぁと思っていると。

[こんな記事](https://numb86-tech.hatenablog.com/entry/2016/11/13/141729)と出会いました。

ほほう、console.errorの処理を上書きするのですね。
というわけで以下のようにしました。

Errorが起こることを正常としたい場合、
`expect().toThrow()`が使えそうです。

```js:src/my-button.test.js
    test('props.value is required', () => {
        expect(() => {
            shallow(<MyButton/>)
        }).toThrow();
    });
```

```jsx:src/my-button.js
import React, {Fragment} from 'react';
import PropTypes from 'prop-types';

const MyButton = (props) => (
    <Fragment>
        <input type="button" value={props.value} />
    </Fragment>
);

MyButton.propTypes = {
    value: PropTypes.string.isRequired
};

export default MyButton;
```

```bash
$ npm run jest
 PASS  src/my-button.test.js
  view
    ✓ button (9ms)
    ✓ props.value is required (1ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.434s
Ran all test suites.
```

## 関数のテスト
onClickのイベントにて、
propsとして定義された関数を実行する関数を用意してテストします。

定義する関数はMockを使い、呼び出されたかのテストをかいてみます。
テストスィート：functionを用意します。

```jsx:src/my-button.test.js
describe('function', () => {
    const mockObj = jest.fn();
    const baseProps = {
        value: 'button'
    };
    
    test('click', () => {
        const props = Object.assign({}, baseProps);
        props.onClick = mockObj;

        const component = shallow(<MyButton {...props} />).instance();
        component.click();

        expect(mockObj.mock.calls.length).toBe(1);
    });
});
```

`shallowObject.instance()`により、ReactComponentを抽出しています。
こうしないと、`click()`が実行できませんでした。

`shallowObject.simulate()`を使う方法もあるみたいですが、
こやつはonXXXXXイベントを実行しているだけに過ぎないみたいなので、
厳密に関数を実行しているわけではないと思い、不採用にしました。

テストします。

```bash
$ npm run jest
 FAIL  src/my-button.test.js
  view
    ✓ button (10ms)
    ✓ props.value is required (1ms)
  function
    ✕ click (5ms)

  ● function › click

    TypeError: component.click is not a function

      37 |         props.onClick = mockObj;
      38 |         const component = shallow(<MyButton {...props} />).instance();
    > 39 |         component.click();
         |                   ^
      40 |         expect(mockObj.mock.calls.length).toBe(1);
      41 |     });
      42 | 

      at Object.<anonymous> (src/my-button.test.js:39:19)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 2 passed, 3 total
Snapshots:   0 total
Time:        1.569s
Ran all test suites.
```

もちろん失敗です。`click()`を実装していませんので。

次に、`click()`を実装しますが、`props.onClick()`は内部で呼ばないようにします。

```jsx:src/my-button.js
import React, {Component, Fragment} from 'react';
import PropTypes from 'prop-types';

class MyButton extends Component {
    constructor(props) {
        super(props);

        this.click = this.click.bind(this);
    }

    render() {
        return (
            <Fragment>
                <input type="button" value={this.props.value} />
            </Fragment>
        );
    }

    click() {
    }
}

MyButton.propTypes = {
    value: PropTypes.string.isRequired
};

export default MyButton;
```

テストします。

```bash
 FAIL  src/my-button.test.js
  view
    ✓ button (9ms)
    ✓ props.value is required (1ms)
  function
    ✕ click (7ms)

  ● function › click

    expect(received).toBe(expected) // Object.is equality

    Expected: 1
    Received: 0

      38 |         const component = shallow(<MyButton {...props} />).instance();
      39 |         component.click();
    > 40 |         expect(mockObj.mock.calls.length).toBe(1);
         |                                           ^
      41 |     });
      42 | 
      43 |     /*

      at Object.<anonymous> (src/my-button.test.js:40:43)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 2 passed, 3 total
Snapshots:   0 total
Time:        1.515s, estimated 2s
Ran all test suites.
```

エラーの内容が変わりました。
モックの呼び出し回数で怒られています。

`props.onClick()`を呼びます。

```jsx:src/my-button.js
    click() {
        this.props.onClick();
    }
```

テストします。

```bash
$ npm run jest
 PASS  src/my-button.test.js
  view
    ✓ button (9ms)
    ✓ props.value is required (1ms)
  function
    ✓ click (1ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        1.101s
Ran all test suites.
```

いい感じ。

ついでに、`props.onClick()`が指定されなくても動作するように、
テストと実装を行っておきます。

```jsx:src/my-button.test.js
describe('function', () => {
    const mockObj = jest.fn();
    const baseProps = {
        value: 'button'
    };
    
    test('click', () => {
        const props = Object.assign({}, baseProps);
        props.onClick = mockObj;

        const component = shallow(<MyButton {...props} />).instance();
        component.click();

        expect(mockObj.mock.calls.length).toBe(1);
    });

    test('Not found props.onClick', () => {
        const props = Object.assign({}, baseProps);

        const component = shallow(<MyButton {...props} />).instance();

        expect(component.click()).toBe(undefined);
    });
});
```

```jsx:src/my-button.js
    click() {
        if ( this.props.hasOwnProperty('onClick') ) this.props.onClick();
    }
```

## stateのテスト
ボタンの活性|非活性をstateで管理して、
それを確認してみます。

```jsx:src/my-button.test.js
describe('function', () => {
    test('click', () => {
        const props = Object.assign({}, baseProps);
        props.onClick = mockObj;

        const component = shallow(<MyButton {...props} />).instance();

        component.click()
        .then(() => {
            expect(mockObj.mock.calls.length).toBe(1);
            expect(component.state.disabled).toBe('disabled');    
        });
    });

    test('Not found props.onClick', () => {
        const props = Object.assign({}, baseProps);

        const component = shallow(<MyButton {...props} />).instance();

        expect(component.click() instanceof Promise).toBe(true);
    });
});
```
```jsx:src/my-button.js
    render() {
        return (
            <Fragment>
                <input type="button" value={this.props.value} disabled={this.state.disabled} onClick={this.click} />
            </Fragment>
        );
    }

    click() {
        return new Promise(resolve => {
            if ( this.props.hasOwnProperty('onClick') ) this.props.onClick();

            this.setState({
                disabled: 'disabled'
            }, resolve());
        });
    }
```

さて、`click()`がPromiseを返すようになりました。
`setState()`が非同期であるため、`expect()`によるテストが失敗するからです。

### setStateはめんどくさい
stateの確認はできましたが、
実際にボタンのdisabledが変わっているかどうかは上記では読み取れません。

そのため、なんとかReactComponentから抽出できないかと調べていましたが、
いい方法がありませんでした・・・。

`shallowObject.simulate()`でいける！と思ったのですが、
やはり`setState()`の非同期性で引っかかります。

[こちらの記事](https://github.com/airbnb/enzyme/issues/1054)でも似たような話をしており、
`simulate()`使うんじゃないよとおっしゃっておりまする。

`simulate()`後に、
`shallowObject.prop()`でプロパティの確認をしたいのに・・・orz

# 結び
それっぽい体験はできましたが、疑問もいくつか残る内容になりました。

また、ライフサイクルメソッドや、子のカスタムコンポーネントの扱いも気になるところです。
(暇があればやらないと）

調べて行く途中で、[似たようなことをすでにやっている記事](http://tyoshikawa1106.hatenablog.com/entry/2016/06/28/150912)があって、
最初からこっちを読めばよかったと嘆く時期がありました。

あと、なんとなくで選んだ割に、Jestがよかったです。
Reactに関しては、これよりいいテストツールがあるんですかね？

参考になる記事などあれば、誰か教えてください！

# 付録
## flowはテスト用ではない？
propsのバリデーションを調べている時に、
今時はflow！PropTypesなんて古いぜ！と読んだので、使ってみました。

結論としては、
テストでは使えなさそう、でした。

実験手順は下記になります。

```bash:インストールや設定ファイルの作成
npm install -D flow-bin
node_modules/.bin/flow init

npm install -g flow-typed
flow-typed install
```

```lang:.flowconfig
[ignore]
.*/node_modules/.*

[include]

[libs]

[lints]

[options]

[strict]
```

テストファイルとコンポーネントファイルの最初に@flowをつけて

```jsx:src/my-button.test.js
// @flow
import React from 'react';
・・・
```

```jsx:src/my-button.js
// @flow
import React, {Fragment} from 'react';
・・・
```

```bash:flow実行
$ ./node_modules/.bin/flow

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/my-button.js:4:17

Missing type annotation for props.

     1│ // @flow
     2│ import React, {Fragment} from 'react';
     3│
     4│ export default (props) => (
     5│     <Fragment>
     6│         <input type="button" value={props.value} />
     7│     </Fragment>

Found 1 error
```

いい感じに怒られました。
上記だとよくわかりませんが、実際の出力ではpropsの部分が赤字で強調されています。

指摘通り、propsに型付けしてみます。

```jsx:src/my-button.js
// @flow
import React, {Fragment} from 'react';

export default (props: {value: string}) => (
    <Fragment>
        <input type="button" value={props.value} />
    </Fragment>
);
```

```bash:flow実行
$ ./node_modules/.bin/flow
No errors!
```

`jest-runner-flowtype`なるものがあったので、
これが使えないかと、ソース読みながら色々試したものの、
flowの結果をうまく捕まえてくれないので、
とりあえずnpm&nbsp;scriptsで繋げるだけにした。
（私の使い方がよくないのかもしれないけど・・)

```json:package.json
"scripts": {
    "jest": "flow ; jest"
},
```

いざ、必須チェックのテストを書くぞ！
と思ったのですが、

- flowで失敗させるために、テストコードに無意味なコードを書く
- flowを成功させると、テストコードは無意味になる

といった状況になり、使用を断念。

```jsx:無意味なコード
  test('props.value is required', () => {
        const wrapper = shallow(<MyButton/>);
  });
```