TypeScriptでMocha, Chaiを使ったテスト駆動開発
インストール
$ npm install chai mocha ts-node @types/chai @types/mocha --save-dev
参考: Testing TypeScript with Mocha and Chai
しかし、私の場合なぜかTypeScriptをグローバルインストールしているにも関わらず、テスト実行時に「typescriptモジュールが見つからない」とエラーが出てしまうので、ローカルに開発インストールを行いました。よってコマンドは以下になります。
✖ ERROR: Cannot find module 'typescript'
# typescriptが見つからない、とエラーが出る場合のインストール。
$ npm install typescript chai mocha ts-node @types/chai @types/mocha --save-dev
package.json
(typescriptを含めた場合)最小限でこのようなpackage.jsonになるはず。
"scripts"下の"test"コマンド定義部分は"test"ディレクトリの下にあるファイル名が".ts"で終わるファイルを全て変更監視の対象にする場合の例です。対象のファイルが変更された場合には自動でtscによるコンパイルが行われ、テストが実行されます。
{
"name": "testPatterns",
"version": "1.0.0",
"description": "samples for test cases.",
"main": "index.js",
"scripts": {
"test": "mocha --require ts-node/register --watch-extensions ts \"test/**/*.ts\""
},
"author": "@olisheo",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"@types/chai": "^4.2.5",
"@types/mocha": "^5.2.7",
"@types/node": "^12.12.12",
"chai": "^4.2.0",
"mocha": "^6.2.2",
"ts-node": "^8.5.2",
"typescript": "^3.7.2"
}
}
前記した通り、上記は"typescript"がローカルインストールされている状態で、なぜかこれが必要でした。
シンプルなテストで動作確認
$ npm test -- -w
シンプルなテスト
とりあえず一番シンプルなテストで動作を確認する。パスするケースと、失敗するケースを一つづつ用意。
describe('simplest test:', () => {
it('1 + 1 should be 2', () => {
expect(1 + 1).to.equal(2);
});
it('the test should fail because it expects "1 + 1 = 0"', () => {
expect(1 + 1).to.equal(0);
});
});
$ npm test -- -w
> testPatterns@1.0.0 test /Users/user/project/testPatterns
> mocha --require ts-node/register --watch-extensions ts "test/**/*.ts" "-w"
simplest test:
✓ 1 + 1 should be 2
1) the test should fail because it expects "1 + 1 = 0"
1 passing (18ms)
1 failing
1) simplest test:
the test should fail because it expects "1 + 1 = 0":
AssertionError: expected 2 to equal 0
+ expected - actual
-2
+0
想定通り、一つはパスして一つはフェイルしてます。
よく使うパターン
同期処理で例外が投げられたらパス
describe('Typical tests:', () => {
it('immediate exception should synchronously be thrown.', () => {
expect(()=>{
// 想定通りならば例外が発生するケースを記述。例えば下のように例外が投げられればパスする。
// throw new Error('just expected exception.');
}).to.throw();
});
});
非同期処理をawaitで待つ
describe('Typical tests:', () => {
it('using await, timer should successfully expires', async () => {
const expirationMessage = await setTimer(1000);
expect(expirationMessage).equals('OK!');
});
});
// テスト対象の非同期関数。
function setTimer(msec: number): Promise<string> {
return new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve('OK!');
}, msec);
});
}
Promiseを使った非同期。
Promiseをリターンで返すことで、Mochaが持っているPromiseのサポートを使える。しかし表記ミスを避けるために、できる限り前期の__await__を使った表記がいいと思う。ちなみにPromiseをリターンしないと、フェイルするテストがパスしてしまう。
describe('Typical tests:', () => {
it('using promise, timer should always be rejcted after timeout.', () => {
return setRejectionTimer(2000).then((expirationMessage)=>{
expect.fail('test fails because the test case expects rejection.');
}).catch((e)=>{
expect(e).to.equal('NOT OK!');
});
});
});
// テスト対象の非同期関数。
function setRejectionTimer(msec: number): Promise<string> {
return new Promise((resolve, reject)=>{
setTimeout(()=>{
reject('NOT OK!');
}, msec);
});
}
///////////////// これはだめ! /////////////////////////////
describe('Typical tests:', () => {
it('using promise, timer should always be rejcted after timeout.', () => {
// 下は間違い。Primiseはリターンで返さないといけない。
setRejectionTimer(2000).then((expirationMessage)=>{
expect.fail('test fails because the test case expects rejection.');
}).catch((e)=>{
expect(e).to.equal('NOT OK!');
});
});
});
実は上記の動くバージョンでも本質的なテストにはなっていなくて、Promiseがrejectされなかった場合は、expect.fail('test fails because the test case expects rejection.') で例外を発生させているため、expect(e).to.equal('NOT OK!')の条件と合致してテストがパスしているのであって、expect.fail()を削除して例外を発生させてなければ、フェイルするべきテストもパスしてしまう。
非同期はto.throw()が使えなさそう。
以下も機能しない。
describe('Typical tests:', () => {
it('delayed exception should asynchronously be thrown.', () => {
expect(async ()=>{
await setRejectionTimer(3000);
}).to.throw(); // 機能しない。
});
});
タイムアウトを回避したい場合のテスト実行コマンド
実行時間がかかるテストも多いので、タイムアウトを延ばすためのオプションはよく使います。
$ npm test -- -w --timeout 30000
これから
「非同期処理の中で例外が起こること」を正確にアサートするのは、現状難しそうです。テスト対象にPromiseを扱いやすくする__chai-as-promise__なるものがあるらしいので、時間を見つけて今度はそちらをかじってみたいと思います。