React + Redux + TypeScriptでテストを書く

※更新履歴

- webpack2-rc & TS2.1 & @types対応
- webpack2 & TS2.3 対応
- React16-beta & TS2.4 対応

こちらの記事の続編です。
http://qiita.com/uryyyyyyy/items/3ad88cf9ca9393335f8c

## 問題提起

今時、型チェックのない言語とか使いたくないですよね！（3回目）
とはいえ型があってもテストは必要です。
reduxはテストしやすさももちろん考慮した設計になっていますので、見ていきましょう。

## 環境

- NodeJS 8.2
- React 16.0-beta
- TypeScript 2.4
- webpack
- karma
- enzyme
- jasmine

## 構成

こちらをご参照ください。
https://github.com/uryyyyyyy/react-redux-sample/tree/redux-test

テストコード以外はこちらの記事そのままです。
http://qiita.com/uryyyyyyy/items/3ad88cf9ca9393335f8c

## テストを走らせるまでの準備

ここでは、以下の構成でテストを行います。

- テストランナー: karma
  - 実際にブラウザ上でテストスクリプトを走らせて動作確認をする
  - Angular2のデファクト
- テストフレームワーク: jasmine
  - 同じくAngular2のデファクト
  - mochaと比べて初期装備が豊富で楽

```
npm install karma-webpack karma-mocha-reporter karma-jasmine karma-chrome-launcher karma jasmine-core @types/jasmine --save-dev
```

そして、karmaの設定を書いていきます。

```js:karma.conf.js
const args = process.argv;
args.splice(0, 4);

const polyfills = [];

const files = polyfills.concat(args);

module.exports = (config) => {
  config.set({
    basePath: '',
    frameworks: ['jasmine'],
    files: files,
    preprocessors: {
      '**/*.spec.ts': ['webpack'],
      '**/*.spec.tsx': ['webpack']
    },
    mime: {
      'text/x-typescript': ['ts','tsx']
    },
    webpack: {
      resolve: {
        extensions: ['.ts', '.js', ".tsx"]
      },
      module: {
        rules: [
          {
            test: /\.tsx?$/,
            use: [
              {loader: "ts-loader"}
            ]
          }
        ]
      }
    },
    webpackMiddleware: {
      stats: 'errors-only',
      noInfo: true
    },
    reporters: ['mocha'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: false,
    browsers: ['Chrome'],
    singleRun: true,
    concurrency: Infinity
  })
};
```

特に説明はしませんが、引数で受けたテストファイルをwebpackでバンドルしてJSに直して、Chrome上でテストを実行しています。

package.jsonはこのような形で、

```
  "scripts": {
    "test:unit": "karma start karma.conf.js",
    "test:all": "karma start karma.conf.js **/*.spec.tsx **/*.spec.ts"
  },
```

`npm run test:all`すると、引数に全テストコードが突っ込まれてテストされます。

unitの方ではテスト対象コードを自分で指定すると、そのテストだけ実行してくれます。

例： `npm run test:unit ./src/counter/__tests__/ActionDispatcher.spec.ts ` など。

こういう指定の仕方にしている理由は、テストコードが増えると全部をテスト流すのは時間がかかるからです。

## moduleのテスト

まずは、一番簡単なmodule(ActionCreator / Reducer)のテストから始めましょう。
なぜ簡単かというと、基本的にReducerは副作用のない関数になるように作っているからです。
（また、ActionCreatorも非同期処理を変に混ぜなければActionを返すだけの関数になるはずです）

### module.spec.ts

```ts:src/counter/__tests__/module.spec.ts
import reducer, {decrementAmount, CounterState, incrementAmount} from '../module'

describe('counter/module', () => {
  it('INCREMENT', () => {
    const state: CounterState = {num: 4}
    const result = reducer(state, incrementAmount(3))
    expect(result.num).toBe(state.num + 3)
  })

  it('DECREMENT', () => {
    const state: CounterState = {num: 4}
    const result = reducer(state, decrementAmount(3))
    expect(result.num).toBe(state.num - 3)
  })
})
```

ここでは2つのテストを書いています。
一つは `incrementAmount` によって加算のActionが作られてreducerに渡ってきた時にちゃんと加算されるか、もうひとつは同様の流れで減算されるかです。

reducerはstateを受け取ってstateを返すだけなので確認が簡単ですね。

### 実行

`npm run test:unit ./src/counter/__tests__/module.spec.ts `

## ActionDispatcherのテスト

ActionDispatcherというのはreduxにある概念ではなく、 [ReduxでのMiddleware不要論](http://qiita.com/uryyyyyyy/items/d8bae6a7fca1c4732696) という記事の中で筆者が提唱しているアプリロジック置き場（非同期処理含む）になります。
やっていることはコンテナ層で非同期処理を実行してるようなものですが、テストしやすいようにクラスを分けていました。

### ActionDispatcher.spec.ts

```ts:src/counter/__tests__/ActionDispatcher.spec.ts
import {incrementAmount} from '../module'
import {ActionDispatcher} from '../Container'

describe('ActionDispatcher', () => {

  it('increment', () => {
    const spy: any = {dispatch: null}
    spyOn(spy, 'dispatch')
    const actions = new ActionDispatcher(spy.dispatch)
    actions.increment(100)
    expect(spy.dispatch.calls.count()).toEqual(1)
    expect(spy.dispatch.calls.argsFor(0)[0]).toEqual(incrementAmount(100))
  })
})
```

`increment`というテストをしていますが、これは、dispatchメソッドをモックしてあげることで、ActionDispatcherのメソッドを叩くとちゃんと適切なactionがdispatchされているかを確認しています。
この例だと、incrementAmountで作られるActionと同じものがdispatchで呼ばれているのを確認しています。

### 実行

`npm run test:unit src/counter/__tests__/ActionDispatcher.spec.ts`

## Componentのテスト

### 準備

reactのテストとして実質デファクト化しているenzymeを用います。domを描画するときは裏でreact-test-rendererを呼んでいるのでそちらも依存に含めます。

```
npm install --save-dev react-test-renderer@16.0.0-beta.1 enzyme @types/enzyme
```

### Counter.spec.tsx

```ts:src/__test__/Counter.spec.tsx
import * as React from 'react'
import {Counter} from '../Counter'
import {shallow} from 'enzyme'
import {CounterState} from '../module'
import {ActionDispatcher} from '../Container'

describe('Counter', () => {

  it('rendering', () => {
    const actions:any = {}
    const state: CounterState = {num: 1}
    const wrapper = shallow(<Counter value={state} actions={actions} />)
    expect(wrapper.find('p').at(0).prop('children')).toBe('score: 1')
  })

  it('click', () => {
    const actionSpy = new ActionDispatcher(null!)
    spyOn(actionSpy, 'increment')
    const state: CounterState = {num: 0}
    const wrapper = shallow(<Counter value={state} actions={actionSpy} />)
    wrapper.find('button').at(0).simulate('click')
    expect(actionSpy.increment).toHaveBeenCalledWith(3)
  })
})
```

1つめのテストでは、p要素にレンダリングされた文字が本当に期待通りかをテストしています。
ここでは、numに1を入れているので、 「score: 1」と出るのが正しいです。

２つめのテストは、ボタンをclickしてみて期待通りの関数が呼び出されるかを確認しています。ここでは１番目のボタンなので`increment 3` 的なものが呼ばれて欲しいんですがその通りになっていますね。

### 実行

`npm run test:unit ./src/counter/__tests__/Counter.spec.ts`

## まとめ

テストもけっこうわかりやすく書けたのではないでしょうか？
どこでどんなオブジェクトが返ってくるのか、TypeScriptなら型でわかるのでわかりやすいですね。テストのモックも作りやすいかもしれません。

次は非同期処理の書き方とそのテストです。