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

  • 15
    いいね
  • 0
    コメント

※更新履歴

  • 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の設定を書いていきます。

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

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不要論 という記事の中で筆者が提唱しているアプリロジック置き場(非同期処理含む)になります。
やっていることはコンテナ層で非同期処理を実行してるようなものですが、テストしやすいようにクラスを分けていました。

ActionDispatcher.spec.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

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」と出るのが正しいです。

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

実行

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

まとめ

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

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