mochaによる非同期のテストの書き方

テストフレームワークmochaでの、非同期のテストの書き方について。

mochaの導入方法などはこちらを参照。
1年近く前のエントリだが、そんなに間違ったことは書いていないはず。

なぜ非同期のテストには工夫が必要なのか

mochaでは、doneを使うことで、簡単に非同期のテストを書ける。

しかし、doneについて説明する前に、そもそもなぜdoneを使う必要があるのかを説明する。
これは、非同期のテストに工夫が必要である理由の説明でもある。

テストのなかで非同期の関数を使うと、非同期の処理が行われる前にテストが終わってしまう。
非同期の処理を待つことなくテストが終了してしまうため、テストが機能しない。
これが、非同期のテストには工夫が必要な理由である。

fs.readFile()を例に、具体的に説明する。
この関数は指定したファイルを読み込むものだが、非同期で実行される。

const fs = require('fs');

fs.readFile('./hoge.txt', 'utf8', (err, data) => {
  if(err) throw err;
  console.log(data);
})

console.log('finish!');

読み込ませるファイルを用意した上で上記のコードを実行すると、finish!と表示された後に、読み込んだファイルの内容が表示される。
つまり、fs.readFile()の結果を待つことなく、プログラムは進んでいく。

テストでも同じように動くため、問題が発生する。

const assert = require('assert');
const fs = require('fs');

describe('async test', () => {
  it('readFile', () => {
    fs.readFile('./hoge.txt', 'utf8', (err, data) => {
      assert(false);  // 本来はここでエラーとなる
    })
  })
});

assert(false)としているので、本来はここでエラーとなり、テストはパスしない。
だが実際には、このテストコードを実行するとパスしてしまう。
fs.readFile()のコールバックの実行を待つことなくプログラムが進んでいき、assert(false)が呼び出される前にテストが終わってしまうからだ。

これを解決し、テストできるようにするのが、doneである。

const assert = require('assert');
const fs = require('fs');

describe('async test', () => {
  it('readFile', (done) => {
    fs.readFile('./hoge.txt', 'utf8', (err, data) => {
      assert(false);
      done(); // ここでテストが終了する
    })
  })
});

使い方は簡単で、itの仮引数にdoneを渡せばいい。
そうすれば、そのテストコードでdone()が呼ばれるまではテストは終了せず、コールバックが実行され、assert(false)も実行される。

doneを使ったPromiseのテスト

Promiseのテストも、doneによって書くことが出来る。

下記は、引数を足し合わせた結果を非同期で返す関数asyncAdd()のテスト。
先程と同様にdone()が呼ばれるまで待つことで、非同期の結果についてテスト出来る。

const assert = require('assert');

function asyncAdd(a, b) {
  return new Promise((resolve) => {
    resolve(a+b);
  });
};

describe('async test', () => {
  it('Promise', (done) => {
    let result = 0;
    asyncAdd(2, 3).then((result) => {
      assert(result === 5);
      done();
    });
  })
});

しかし、このコードには一つ問題がある。テストが通らなかった際にdone()が呼ばれず、テストが正常に終了しない。

テストを次のように書き換えてテストが通らないようにした場合、Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.というエラーが発生する。

describe('async test', () => {
  it('Promise', (done) => {
    let result = 0;
    asyncAdd(2, 3).then((result) => {
      assert(result === 9); // ここで例外が発生するため、Rejectedなpromiseオブジェクトが返される
      done(); // これは呼ばれない
    });
  })
});

これを回避するためには、以下のように書く。

describe('async test', () => {
  it('Promise', (done) => {
    let result = 0;
    asyncAdd(2, 3).then((result) => {
      assert(result === 9);
    }).then(done, done);
  })
});

テストがパスすれば最後のthen()の第一引数のdone()が、失敗した場合は第二引数のdone()が呼ばれるため、done()が呼ばれないという状況を回避できる。

doneを使わないPromiseのテスト

mochaでは、done()を使わずにPromiseのテストを書くことも出来る。

テストするPromiseオブジェクトをreturnするようにして、そのthen()のなかにアサートを書く、という方法である。

以前書いたJestでの方法と同じだ。
JestでReactのテストをする(6) 非同期のテスト

const assert = require('assert');

function asyncAdd(a, b) {
  return new Promise((resolve) => {
    resolve(a+b);
  });
};

describe('async test', () => {
  it('return Promise', () => {
    let result = 0;
    return asyncAdd(2, 3).then((result) => {
      assert(result === 9);
    });
  })
});

参考資料