Posted at

jest で非同期関数をテストするときの注意点

不適切な書き方をすると、落ちるべき(誤った)テストが通過する場合がある。


結論


コールバック


  • テスト関数の引数にdoneを入れる

  • コールバック関数内の最後でdone() する


Promise


  • Promise をreturnするか、async / await で扱う

  • 異常系のテストでは、catch 句の外側に expect.assertions(n) を書くか、expect(Promise).rejectsを使う


リンク



問題

適切な書き方をしなかった場合、非同期処理(内の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!');
});
});

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返してもらうようにして

async/awaitするなりresolves/rejectsするなりすると

expectが関数の外に出て

優しい世界が訪れる気がします。