問題提起
なるべくjsdom上でテスト出来ることが望ましいですが、たまにブラウザ上でテストしたくなったり、jquery pluginなどcommonjs化してない(行儀の悪い)ライブラリを使うこともあると思います。その時のテストをkarmaでやってみます。
余談ですが、これをやり始めたのは、Angular2でもテストツールでkarmaを使うっぽいので予習しておこうと思った次第です。
環境
- NodeJS 5.X~
- React 15.1
- TypeScript 1.8
commonjs化されてない(行儀の悪い)ライブラリとして
- Materialize-css(jquery依存)
を取り入れてみます。
ソースコード全体はこちら
ゴール
- karmaの挙動を理解する
- Typescript + commonJSなコードを実ブラウザでUnitTestできる
- ブラウザで読み込ませる必要のあるコードを含んだテストができる。
karmaの動作の概要
(筆者の理解です。)
- karmaが起動してブラウザを立ち上げる
- (polyfillなどの)事前に必要なライブラリなど読み込む。
- tsやcommonjsのコードはそのままでは読めないのでpreprocesserがjsを生成するのでそれを読む
- webpackを用いてるので、ts→jsの変換とバンドルまでを行ってくれる。
- エントリポイントはテストコードになり、それが依存するコードが順にバンドルされる
- そのjsをブラウザに読ませて、フレームワークがテストを走らせる。
- 今回はmochaを使っている。
- アサートライブラリはchaiを使っていて、これはwebpackがバンドルしてくれる。
- 結果を出力する
動作の詳細とコード
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
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
module.exports = {
resolve: {
extensions: ['', '.ts', '.js', ".tsx"]
},
module: {
loaders: [
{ test: /\.tsx?$/, loader: "ts-loader" }
]
}
};
karma実行時には、エントリポイントはkarmaが渡してくれるので記述しなくても動作します。
テストコードの方のrequireを辿っていくことで自然とソースコードも含まれるので、それらが個別にバンドルされます。
(テストコードのファイル数分バンドルされるので、テストコードが多いと解決に時間かかりそうです。
たぶん、テスト対象を細かく指定するくらいしか対処法がない。。)
テストコード
基本的には上記で設定完了です。意外とシンプルでした。
一応、テストコードも見てみます。(普通のmochaのコードです。)
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でテストを書く
以前、こちらの記事で書いたコードがそのまま使えます。(chaiとかも、karmaにframeworkとして組み込ませるやり方もあるっぽいのですが、こちらで普通に動いたのでそのまま使っています。)
注意点
jsdomでは、node上で動作するので、ソース/テストコードにnodejs依存の記述があっても動きましたが、karmaだと実行環境がブラウザなので、それらは動きません。例えば、
- fsなど設定ファイルを読みこむ系のテスト
- nockなど、httpサーバをモックするテスト
- Angularでは、フレームワーク側でテストのサポートをしています($httpのDI)。
これは当たり前といえば当たり前で、回避方法はあるのですが、一応共有まで。
まとめ
karma、実ブラウザでテストしてくれるし速度も問題ないので、テストに使うのは良いかもしれないです。