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()=>{constexpirationMessage=awaitsetTimer(1000);expect(expirationMessage).equals('OK!');});});// テスト対象の非同期関数。functionsetTimer(msec:number):Promise<string>{returnnewPromise((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.',()=>{returnsetRejectionTimer(2000).then((expirationMessage)=>{expect.fail('test fails because the test case expects rejection.');}).catch((e)=>{expect(e).to.equal('NOT OK!');});});});// テスト対象の非同期関数。functionsetRejectionTimer(msec:number):Promise<string>{returnnewPromise((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()=>{awaitsetRejectionTimer(3000);}).to.throw();// 機能しない。});});
タイムアウトを回避したい場合のテスト実行コマンド
実行時間がかかるテストも多いので、タイムアウトを延ばすためのオプションはよく使います。
$ npm test---w--timeout 30000
これから
「非同期処理の中で例外が起こること」を正確にアサートするのは、現状難しそうです。テスト対象にPromiseを扱いやすくするchai-as-promiseなるものがあるらしいので、時間を見つけて今度はそちらをかじってみたいと思います。