Mocha, Chaiを使ったテストの表記パターン

# TypeScriptでMocha, Chaiを使ったテスト駆動開発
## インストール
```bash:TDDをサポートしてくれるパッケージのインストール。
$ npm install chai mocha ts-node @types/chai @types/mocha --save-dev 
```
参考: [Testing TypeScript with Mocha and Chai](https://42coders.com/testing-typescript-with-mocha-and-chai/)

しかし、私の場合なぜかTypeScriptをグローバルインストールしているにも関わらず、テスト実行時に「typescriptモジュールが見つからない」とエラーが出てしまうので、ローカルに開発インストールを行いました。よってコマンドは以下になります。

```bash:テスト実行時のエラー例
✖ 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によるコンパイルが行われ、テストが実行されます。

```json:package.json
{
  "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"がローカルインストールされている状態で、なぜかこれが必要でした。

## シンプルなテストで動作確認
```bash:テストの実行コマンド
$ npm test -- -w
```
### シンプルなテスト
とりあえず一番シンプルなテストで動作を確認する。パスするケースと、失敗するケースを一つづつ用意。

```typescript
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);
  });
});
```
```bash:実行と結果
$ 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
```

想定通り、一つはパスして一つはフェイルしてます。

## よく使うパターン
### 同期処理で例外が投げられたらパス
```typescript
describe('Typical tests:', () => {
  it('immediate exception should synchronously be thrown.', () => {
    expect(()=>{
      // 想定通りならば例外が発生するケースを記述。例えば下のように例外が投げられればパスする。
      // throw new Error('just expected exception.');  
    }).to.throw();
  });
});
```
### 非同期処理をawaitで待つ
```typescript
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をリターンしないと、フェイルするテストがパスしてしまう。

```typescript
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()が使えなさそう。
以下も機能しない。

```typescript:非同期関数内でrejectが発生してもテストはパスしない。
describe('Typical tests:', () => {
  it('delayed exception should asynchronously be thrown.', () => {
    expect(async ()=>{
      await setRejectionTimer(3000);
    }).to.throw(); // 機能しない。
  });
});
```
### タイムアウトを回避したい場合のテスト実行コマンド
実行時間がかかるテストも多いので、タイムアウトを延ばすためのオプションはよく使います。

```bash:--timeoutパラメーターで30秒のタイムアウトを指示した場合。
$ npm test -- -w --timeout 30000
```

## これから
「非同期処理の中で例外が起こること」を正確にアサートするのは、現状難しそうです。テスト対象にPromiseを扱いやすくする__chai-as-promise__なるものがあるらしいので、時間を見つけて今度はそちらをかじってみたいと思います。