かれこれ1年ほど、複数のプロジェクトでmocha/chai/sinonを使ったテストを使ってきましたが、テストコードのrequire部分から秘伝のタレっぽい香りがただよってきました。
特に、chaiのplugin関連は混乱していて、chai-as-promisedを使っているのに、実際のテストは全てasync関数で書いてるとか、謎な状態になりつつあるので、将来の自分を救うために、現時点(2019年1月)で使っているpluginを逆引き形式でまとめておきます。
はじめに
この記事にはchai/mocha/sinonがなにものなのかとか、環境構築方法とかは一切扱っていません。必要に応じて他の記事を参照してください。
本記事中のコード例は以下のお約束のもとに書かれています。コピペする際には十分に気をつけてください。
- テストコードの冒頭には以下のようなコードが書かれているものとします。
const chai = require("chai");
const { expect } = require("chai");
- 各セクションにある 使い方 の章はテストコード内でそのpluginを使うために必要なコードを書いています。
- 各セクションにある サンプル の章は実際にそのpluginを使ったテストコードの例を書いています。
- サンプルに書いてあるテスト対象の関数名は
testee()
とします。 - 実行環境はnode.jsを前提としています。この記事で紹介しているpluginの中にはブラウザ環境でも動作するものもありますが(むしろそっちの方が多いかも?)ブラウザ環境でのセットアップ方法は各プラグインのドキュメント等を参照してください。
イテレータ関数のテストをしたい
chaiIterator を使います。
インストール方法
npm install -D chai-iterator
chaiIteratorは一時期メンテナンスがされていなくて、chaiの最新版では動かない状況になっていましたが、forkしてアップデート版を作っていた@harrysarson 氏が2018年の秋頃にパッケージのメンテナになったようです。過渡期の記事などでは、githubのリポジトリ&ブランチ名を指定してインストールするように指示されているものもありますが、現在はこちらに書いたようにパッケージ名だけでインストールできます。
使い方
chai.use(require("chai-iterator"));
サンプル
expect(testee()).to.deep.iterate.over([expected1, expected2...]);
expected1,expected2...の部分にはtesteeが呼ばれる度に返す値を順に書いておきます。
async関数がrejectされるテストケースを書きたい
chai-as-promised を使います。
インストール方法
npm install -D chai-as-promised
使い方
chai.use(require("chai-as-promised"));
サンプル
expect(testee()).to.be.rejectedWith(expected);
expectedの部分にはtesteeがreject()する時の引数を書いておきます。
---- 2019/2/22 追記 ----
mochaから使う場合は次のようにreturnする必要があります。
it("should be rejected", ()=>{
return expect(testee()).to.be.rejectedWith(expected);
})
おまけの補足ですが、rejectedWithの引数はchai標準のthrowと同様のものを受け取るので
第1引数のエラーは、単純なErrorじゃなくてTypeErrorとかの派生クラスを指定することもできるし
第2引数に、エラーメッセージのregexpを指定することもできます。
この辺をある程度細かく絞っておかないと、testeeの中でtypoがあってundefined referenceになっているだけなのにテストを通ったりするので要注意です。
---- ここまで ----
mochaが非同期テストをサポートしたので、resolveされるケースをテストしたいのであれば
it("should resolved with hoge", async ()=>{
expect(await testee()).to.rqual("hoge");
});
みたいな書き方ができるのですが、rejectされるケースをchai-as-promisedを使わずに書こうとすると
it("should resolved with hoge", ()=>{
try{
await testee();
fail();
}catch(e){
expect(e).to.rqual("hoge");
}
});
のようなコードになってしまって冗長です。
ディレクトリ/ファイル操作を行う関数をテストしたい
I/O部分がstubでも十分なテストができるのであればstubを使う方が良いのですが、負荷テストとかでどうしても実際にファイルを生成してテストしたいって時がありますよね?
そんな時はchai-fs を使います。
インストール方法
npm install -D chai-fs
使い方
chai.use(require("chai-fs"));
サンプル
it("should remove foo and create bar/baz", async ()=>{
await testee();
expect("foo").not.to.be.path();
expect("bar").to.be.a.directory();
expect("bar/baz").to.be.a.file();
});
chai-fsはwithとかandとかで色々と付け足すことができてbar/bazの例であれば次のような書き方もできます。
expect("bar").to.be.a.directory().with.files(["baz"]);
JSONが想定した形式のものかどうかテストしたい。
JSONを返すような関数の戻り値を確認する時に、JSON-schemaで形式を定義しておけば、chai-json-schemaを使ってテストできます。
インストール方法
npm install -D chai-json-schema
使い方
chai.use(require('chai-json-schema'));
サンプル
it("should return JSON", async ()=>{
expect(await testee()).to.jsonSchema(expected);
});
expectedの部分は戻り値のvalidationに使うjson-schemaを書いておきます。
このpluginは、前述のchai-fsから呼び出すことができて
expect("hoge").to.be.a.file().with.json.using.schema(schema);
なんていう書き方もできます。
call backルーチンをテストしたい
非同期関数はなるべくasyncを使いたい派ですが、実行時間が長くなったり、返す値が大きいものについては、event emitterでなんとかする必要があるので、どうしてもcall backルーチンからは逃げられません。
テストではcall backルーチンの中で何か処理をすることは無いので、sinonのstubを渡すことが多いのですが、sinon-chaiを使えば、stub関数(spyも)のアサーションが書きやすくなります。
インストール方法
npm install -D sinon-chai
使い方
chai.use(require("sinon-chai"));
サンプル
it("should call cb", ()=>{
testee(cb);
expect(cb).to.have.been.calledWith(false);
});
pluginのドキュメントにもありますが、sinon-chaiを使わずにstub関数をテストしようとすると
expect(cb.calledWith(false)).to.be.true;
みたいな書き方になってしまって、期待してる結果がtrueなんだかfalse分からんなんてことも稀に良くあります。
個人的には、socketIOのイベントハンドラをテストする時にack用のcall backをstubにして処理完了時にcall backルーチンを呼び忘れていないか確認するのに良く使っています。
配列の中身を確認したい
chai-thingsを使います。
インストール方法
npm install -D chai-things
使い方
chai.use(require("chai-things"));
サンプル
it("should return array of true", ()=>{
expect(testee()).to.all.eql(true);
});
主に負荷テストで、Promise.all()から大量に呼び出したasync関数の戻り値をto.all.eql()
に渡して、全部正常終了しているか確認するのに使っています。