[Node] Mochaで非同期処理の例外ハンドリング

非同期処理(Promise とか、最近では async/await とか)内で発生した例外を mocha でテストする場合、いくつかの方法があるのでまとめてみる。

おさらい

その前に少しおさらい。

同期処理での例外のテストと、例外をスローしない非同期処理のテストを見てみる。

同期処理での例外テスト

chai の expect を使って例外のスローを判定する。expect には 関数 を渡す必要があるので、expect(expectedFunction)という形になっている。

const { expect } = require('chai')

describe('同期処理テスト', () => {
it('例外がスローされること', () => {
expect(expectedFunction).to.throw()
})
})

検査対象の関数に引数がある場合は、以下のように無名関数でラップしてあげればよい。

expect(() => expectedFunction(args)).to.throw()

例外をスローしない非同期処理のテスト

単純に非同期関数をテストする場合だ、mocha は async/await が使えるので非常にシンプルに書くことができる。

const { assert } = require('chai')

describe('非同期処理テスト', () => {
it('actualがexpectedであること', async () => {
const actual = await expectedFunction()
assert.equal(actual, expected)
})
})

done を使っても書けるが、若干冗長になる。

const { assert } = require('chai')

describe('非同期処理テスト', () => {
it('actualがexpectedであること', done => {
expectedFunction()
.then(actual => {
assert.equal(actual, expected)
done()
})
.catch(err => {
assert.fail()
done()
})
})
})

mocha は Promise を return すると、mocha はちゃんと Promise のテストであると判断して処理してくれる。これを利用するともう少し簡素に記述できる。

const { assert } = require('chai')

describe('非同期処理テスト', () => {
it('actualがexpectedであること', () => {
return expectedFunction().then(actual => {
assert.equal(actual, expected)
})
})
})

おさらいお終い。

非同期処理の例外ハンドリング

さて本題だ、以下の関数のテストを行いたい。

const fs = require('fs')
const { promisify } = require('util')

const getContent = path => {
return promisify(fs.readFile)(path)
}

fs.readFile を promisify した関数で、ファイルの内容(content)を Promise で返す。こんな感じで呼び出すことができる。

const content = await getContent('filepath')
console.log(content)

もしくは

getContent('filepath').then(content => console.log(content))

単純に async/await を付けてみる

it のコールバックに async、expect に await を付けてみる。尚エラーを発生させるので、missing.txtという存在しないファイルを指定する。

it('throw error', async () => {
await expect(() => {
getContent('./missing.txt')
}).to.throw()
})

結果、怒られた

    1) throw error
(node:19780) UnhandledPromiseRejectionWarning: Error: ENOENT: no such file or directory, open './missing.txt'
(node:19780) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 2)
(node:19780) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.


0 passing (28ms)
1 failing

1) file load
throw error:
AssertionError: expected [Function] to throw an error

Promise の Reject をハンドルしろ、とのことだ

try/catch してみる

expect を介してだと async/await がうまく動かなかった。今度は expect を使わず、try/catch で判定してみる。

it('throw error', async () => {
try {
await getContent('./missing.txt')
assert.fail()
} catch (e) {
assert.equal(e.code, 'ENOENT')
}
})

これはパスする。

file load
√ throw error


1 passing

しかしあまり上手く無い感がある。assert.fail() とか e.code で判定している所とか。catch した Error オブジェクトを assert するのではなくて、できれば例外ハンドル自体を assert したい。

言い換えると例外がスローされたことを chai が検知して、assert が OK であると判断して貰いたい。上記の例だと、例外の検知はテストコードで行っているのでちょっとイケてないよね。ということだ。

Promise を return する

おさらいの最後に書いた、Promise を return する形で書くと若干簡素になる。

it('throw error', () =>
getContent('./missing.txt').then(
() => assert.fail(),
err => assert.equal(err.code, 'ENOENT')
))

ただ前述の問題は解決していない。やっぱり例外ハンドル自体を assert したい。

chai-as-promised を使う

chai-as-promised という chai のプラグインがあり、これを使うと Promise の判定がより直感的に記述できるようになる。

こんなに簡単になった。

const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')
chai.use(chaiAsPromised)

it('throw error', () => expect(getContent('./missing.txt')).to.be.rejected)

これなら例外ハンドルが飛んだことを be.rejected で判断してくれるので、大満足である。

エラーの型まで見たい場合は be.rejectedWith()を使えばいい

const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')
chai.use(chaiAsPromised)

it('throw error', () =>
expect(getContent('./missing.txt')).to.be.rejectedWith(Error))

参考