🧪
jest で非同期関数をテストするときの注意点
不適切な書き方をすると、落ちるべき(誤った)テストが通過する場合がある。
結論
コールバック
- テスト関数の引数に
done
を入れる - コールバック関数内の最後で
done()
する
Promise
- Promise を
return
するか、async
/await
で扱う - 異常系のテストでは、
catch
句の外側にexpect.assertions(n)
/expect. hasAssertions()
を書くか、expect(Promise).rejects
を使う
リンク
- Testing Asynchronous Code · Jest
- jest - Necessary to use expect.assertions() if you're awaiting any async function calls? - Stack Overflow
問題
適切な書き方をしなかった場合、非同期処理(内のexpect
)が実行される前にテスト関数が終了してしまう。
Jest はテスト関数が(エラーなく)終了した時点で通過とみなすため、誤ったテストが通過とみなされる可能性がある。
どうするのか
(前提) 以下の例で扱うテスト対象の関数
いずれもフラグisSuccess
に応じて'hoge'
かError('error!')
を返す。
コールバック
const someCallback = (isSuccess, cb) => {
setTimeout(() => {
const err = new Error('error!');
const data = 'data';
return isSuccess ? cb(null, data) : cb(err, null);
}, 200);
};
Promise
const somePromise = isSuccess => {
return new Promise((resolve, reject) => {
const err = new Error('error!');
const data = 'data';
isSuccess ? resolve(data) : reject(err);
});
};
❌ な書き方と ⭕ な書き方
コールバック
❌ テスト関数の引数にdone
を入れていない
❌ コールバック関数をdone()
で終わらせていない
// pass!
test('no done ', () => {
someCallback(false, (err, data) => {
expect(data).toBe('fuga'); // data === hoge
});
});
⭕ テスト関数の引数にdone
を入れている
⭕ コールバック関数をdone()
で終わらせている
// fail!
test('done exists', done => {
someCallback(false, (err, data) => {
expect(data).toBe('fuga');
done();
});
});
Promise
Promise
❌ Promise をreturn
していない
// pass!
test('no return', () => {
somePromise(false).then(() => {
expect(data).toBe('fuga');
});
});
⭕ Promise をreturn
している
// fail!
test('return exists', () => {
// error!
return somePromise(false).then(() => {
expect(data).toBe('fuga');
});
});
直接関係はないが、Promise はresolves
/rejects
を使うとすっきり書ける
// fail!
test('resolves 1', () => {
return expect(somePromise(false)).resolves.toBe('fuga');
});
// pass!
test('resolves 2', () => {
return expect(somePromise(true)).resolves.toBe('hoge');
});
async
/ await
❌ await
していない
- というか素の Promise の書き方とごっちゃになっている
- (実務で見た)
// pass!
test('no await', async () => {
somePromise(false).then(result => {
expect(result).toBe('fuga');
});
});
⭕ await
している
// fail!
test('await exists', async () => {
const result = await somePromise(false); // error!
expect(result).toBe('fuga');
});
なお、await
し忘れただけならexpect
が実行されるので落ちる
// fail!
test('forgot await', async () => {
const result = somePromise(false);
expect(result).toBe('fuga'); // Comparing two different types of values. Expected string but received object.
});
異常系
expect
が実行されない場合テストが pass してしまう
❌ expect.assertions(n)
していない
// pass!
test('no expect assertions', () => {
return somePromise(true).catch(e => {
expect(e.message).toBe('error!'); // resolveされたのでcatch句に入らない
});
});
⭕ expect.assertions(n)
している
- いくつの
expect
が実行されるべきかテストしてくれる -
expect
が条件分岐の中にあるならば、異常系に限らず書いておくと安心
// fail!
test('expect assertions exists', () => {
expect.assertions(1); // Expected one assertion to be called but received zero assertion calls.
return somePromise(true).catch(e => {
expect(e.message).toBe('error!');
});
});
- expectの数に興味がない場合、「少なくともひとつ
expect
がある」ことをテストするexpect.hasAssertions()
も使える- むしろこっちがメインか
// fail!
test('expect hasAssertions exists', () => {
expect.hasAssertions();
return somePromise(true).catch(e => {
expect(e.message).toBe('error!');
});
});
⭕ rejects
を使っている
// fail!
test('using rejects', () => {
// Expected received Promise to reject, instead it resolved to value "hoge"
return expect(somePromise(true)).rejects.toThrow('error!');
});
// fail!
test('using rejects', async () => {
// Expected received Promise to reject, instead it resolved to value "hoge"
await expect(somePromise(true)).rejects.toThrow('error!');
});
どうしてこうなるの
- Node.js は非同期処理に差し掛かると、その処理を一旦終了し、後続処理を続行する
- 一旦終了した処理はキューにためられ、ほかの通常の同期処理が終わってから実行される
- Jest は、テスト関数がエラーなく終了すると pass として扱う
非同期処理(というかイベントループ)についてはこのへんを読むと理解が深まる気がします。
- 非同期処理と Promise(Deferred)を背景から理解しよう - hifive
- 並列モデルとイベントループ - JavaScript | MDN
- そうだったのか! よくわかる process.nextTick() node.js のイベントループを理解する
- About | Node.js
- Overview of Blocking vs Non-Blocking | Node.js
おわり
みんなコールバックやめてPromise返してもらうようにして
async
/await
するなりresolves
/rejects
するなりすると
expect
が関数の外に出て
優しい世界が訪れる気がします。
Discussion