🧪
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