ReactでもKarmaでテスト書いてみる


## 問題提起

なるべくjsdom上でテスト出来ることが望ましいですが、たまにブラウザ上でテストしたくなったり、jquery pluginなどcommonjs化してない（行儀の悪い）ライブラリを使うこともあると思います。その時のテストをkarmaでやってみます。

余談ですが、これをやり始めたのは、Angular2でもテストツールでkarmaを使うっぽいので予習しておこうと思った次第です。

## 環境

- NodeJS 5.X~
- React 15.1
- TypeScript 1.8

commonjs化されてない（行儀の悪い）ライブラリとして

- Materialize-css（jquery依存）

を取り入れてみます。

ソースコード全体はこちら

https://github.com/uryyyyyyy/react-redux-sample/tree/test-karma

## ゴール

- karmaの挙動を理解する
- Typescript + commonJSなコードを実ブラウザでUnitTestできる
- ブラウザで読み込ませる必要のあるコードを含んだテストができる。

## karmaの動作の概要

（筆者の理解です。）

- karmaが起動してブラウザを立ち上げる
- （polyfillなどの）事前に必要なライブラリなど読み込む。
- tsやcommonjsのコードはそのままでは読めないのでpreprocesserがjsを生成するのでそれを読む
  - webpackを用いてるので、ts→jsの変換とバンドルまでを行ってくれる。
  - エントリポイントはテストコードになり、それが依存するコードが順にバンドルされる
- そのjsをブラウザに読ませて、フレームワークがテストを走らせる。
  - 今回はmochaを使っている。
  - アサートライブラリはchaiを使っていて、これはwebpackがバンドルしてくれる。
- 結果を出力する

## 動作の詳細とコード

### package.json

```package.json
  "devDependencies": {
    "chai": "^3.5.0",
    "karma": "^1.2.0",
    "karma-chrome-launcher": "^2.0.0",
    "karma-mocha": "^1.1.1",
    "karma-webpack": "^1.8.0",
    "react-addons-test-utils": "^15.1.0",
    "redux-mock-store": "^1.1.1",
    "sinon": "^1.17.4",
    "ts-loader": "^0.8.2",
    "webpack": "^1.13.1"
  },
```

関連するのはこのあたりです。
karmaの中でwebpack/mocha/chromeを使うので各種インストールしています。

### karma.conf.js

```js:karma.conf.js
module.exports = function(config) {
  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',


    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['mocha'],


    // list of files / patterns to load in the browser
    files: [
      'node_modules/jquery/dist/jquery.min.js',
      'node_modules/materialize-css/dist/js/materialize.min.js',
      '**/__test__/*.ts*'
    ],


    // list of files to exclude
    exclude: [
    ],


    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
      '**/__test__/*.ts*': ['webpack']
    },


    webpack: require(__dirname + '/webpack.config.karma.js'),


    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['progress'],


    // web server port
    port: 9876,


    // enable / disable colors in the output (reporters and logs)
    colors: true,


    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,


    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,


    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['Chrome'],


    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: true,

    // Concurrency level
    // how many browser should be started simultaneous
    concurrency: Infinity
  })
};
```

jsという名の設定ファイルです。

かいつまんで見ていきましょう。（基本コメントについてるのそのままです）

- basePath
  - 対象とするペースパス。file/excludeとかのベース。
- frameworks
  - どのテストフレームワークを使うか。
  - 細かいのがいくつかありますが、mocha/jasmineあたりだけ抑えておけば良いのでは
- files
  - ブラウザに読み込ませるファイル。記述した順番に読まれるっぽい。
  - watch指定できるので、差分があればテストを再開できる。
- exclude
  - filesで選んだウチの除外対象かな
- preprocessors
  - 上記でtsコードを指定していますが、そのままでは動かないので前処理をする。
  - 今回はwebpackをかけています。
- webpack
  - 上記でwebpackと書いたのでその設定ファイル
  - 上記のtestコードをエントリポイントとして、各種tsをjsへバンドルします。
- browsers
  - テストを動かす対象ブラウザ。別途launcherをインストールしておく必要がある
- singleRun
  - falseにしたら常駐していて、差分あるたびに再テストされるのかな

### webpack.config.js

```js:webpack.config.karma.js
module.exports = {
  resolve: {
    extensions: ['', '.ts', '.js', ".tsx"]
  },
  module: {
    loaders: [
      { test: /\.tsx?$/, loader: "ts-loader" }
    ]
  }
};
```

karma実行時には、エントリポイントはkarmaが渡してくれるので記述しなくても動作します。
テストコードの方のrequireを辿っていくことで自然とソースコードも含まれるので、それらが個別にバンドルされます。
（テストコードのファイル数分バンドルされるので、テストコードが多いと解決に時間かかりそうです。
たぶん、テスト対象を細かく指定するくらいしか対処法がない。。）


## テストコード

基本的には上記で設定完了です。意外とシンプルでした。
一応、テストコードも見てみます。（普通のmochaのコードです。）

```ts:Reducer-test.ts
import {assert} from "chai";
import {counter} from "../Reducer";
import {GlobalState, MyAction, ActionTypes} from "../Models";

describe('reducer test', () => {
    it('INCREMENT', () => {
        const state: GlobalState = {num: 4, loadingCount:0};
        const action: MyAction = { type: ActionTypes.INCREMENT, amount: 3};
        const result = counter(state, action);
        assert.deepEqual(result.num, state.num + 3);
        assert.deepEqual(result.loadingCount, state.loadingCount);
    });

    it('DECREMENT', () => {
        const state: GlobalState = {num: -2, loadingCount:0};
        const action: MyAction = { type: ActionTypes.DECREMENT, amount: 10};
        const result = counter(state, action);
        assert.deepEqual(result.num, state.num - 10);
        assert.deepEqual(result.loadingCount, state.loadingCount);
    });

    it('FETCH_SUCCESS', () => {
        const state: GlobalState = {num: -2, loadingCount:1};
        const action: MyAction = { type: ActionTypes.FETCH_SUCCESS, amount: 10};
        const result = counter(state, action);
        assert.deepEqual(result.num, state.num + 10);
        assert.deepEqual(result.loadingCount, state.loadingCount -1);
    });
});
```

[React + Redux + TypeScriptでテストを書く](http://qiita.com/uryyyyyyy/items/7d4b0ede3f2b973d6951)

以前、こちらの記事で書いたコードがそのまま使えます。（chaiとかも、karmaにframeworkとして組み込ませるやり方もあるっぽいのですが、こちらで普通に動いたのでそのまま使っています。）

## 注意点

jsdomでは、node上で動作するので、ソース/テストコードにnodejs依存の記述があっても動きましたが、karmaだと実行環境がブラウザなので、それらは動きません。例えば、

- fsなど設定ファイルを読みこむ系のテスト
- nockなど、httpサーバをモックするテスト
  - Angularでは、フレームワーク側でテストのサポートをしています（$httpのDI）。

これは当たり前といえば当たり前で、回避方法はあるのですが、一応共有まで。

## まとめ

karma、実ブラウザでテストしてくれるし速度も問題ないので、テストに使うのは良いかもしれないです。