7
6

More than 3 years have passed since last update.

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

Posted at

TypeScriptでMocha, Chaiを使ったテスト駆動開発

インストール

TDDをサポートしてくれるパッケージのインストール。
$ 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によるコンパイルが行われ、テストが実行されます。

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

シンプルなテストで動作確認

テストの実行コマンド
$ 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()が使えなさそう。

以下も機能しない。

非同期関数内でrejectが発生してもテストはパスしない。
describe('Typical tests:', () => {
  it('delayed exception should asynchronously be thrown.', () => {
    expect(async ()=>{
      await setRejectionTimer(3000);
    }).to.throw(); // 機能しない。
  });
});

タイムアウトを回避したい場合のテスト実行コマンド

実行時間がかかるテストも多いので、タイムアウトを延ばすためのオプションはよく使います。

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

これから

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

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6