JavaScript 应用中通常会有大量异步代码。当测试这些代码时,Jest 需要明确知道异步调用的结束时刻,这样它才会进入下一个测试用例。Jest 提供了多种测试异步代码的方法。
回调函数
最常用的异步模式是回调函数。
比如,有一个 fetchData(callback) 函数,获取数据后调用 callback(data)。我们期望返回的数据 data 是字符串 "peanut butter"。
默认情况下,Jest 执行到代码块结尾时就会结束。因此,下面的写法无法满足我们的测试需求:
// 不要这样做!
test('the data is peanut butter', () => {
function callback(data) {
expect(data).toBe('peanut butter');
}
fetchData(callback);
});
fetchData 一旦执行完毕,测试就会中止,很可能尚未执行回调。
我们可以使用 test 的另一种形式解决这个问题。向测试用例的函数新增一个 done 参数后,Jest 会等待 done 被调用,然后再结束本次测试。
test('the data is peanut butter', done => {
function callback(data) {
try {
expect(data).toBe('peanut butter');
done();
} catch (error) {
done(error);
}
}
fetchData(callback);
});
如果 done 不被调用,则测试失败(超时错误),这种行为符合预期。
如果 expect 语句失败,它会抛出异常,导致无法调用 done()。如果我们希望在日志中查看失败原因,需要将 expect 包裹在 try 语句块,并且将 catch 块的参数传入 done。否则,我们只能拿到普通的超时错误,无法得到 expect(data) 接收的具体值。
注意:不要混合使用
done()和 promise,因为这可能导致内存泄漏。
Promise
如果代码中用到 promise,可以使用另一种更直接的方法处理异步测试。测试函数如果返回 promise,Jest 会等待 promise 落地(resolved)。如果 promise 被拒,测试自动失败。
比如,fetchData 不使用回调,而是返回一个 promise,成功的值是字符串 'peanut butter'。可以如下测试:
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
一定要记得返回 promise - 如果忘记编写 return 语句,测试会提前结束,导致无法执行 .then() 中的回调。
如果期望 promise 被拒,可以使用 .catch 方法。务必记得增加 expect.assertions 用于确保断言调用的次数。否则,一次成功的 promise 无法让测试失效(这是不符合预期的)。
test('the fetch fails with an error', () => {
expect.assertions(1);
return fetchData().catch(e => expect(e).toMatch('error'));
});
.resolves / .rejects
也可以在 expect 语句中使用 .resolves 匹配器,Jest 会等待 promise 落地。如果 promise 被拒,则测试自动失败。
test('the data is peanut butter', () => {
return expect(fetchData()).resolves.toBe('peanut butter');
});
记得一定要返回断言,原因和上面的 promise 一样,不再赘述。
如果期望 promise 被拒,使用 .rejects 匹配器。它的用法同 .resolves 匹配器类似。如果 promise 成功,则测试自动失败。
test('the fetch fails with an error', () => {
return expect(fetchData()).rejects.toMatch('error');
});
Async / Await
另外,可以在测试中使用 async 和 await。在 test 传入测试函数时,增加 async 关键词,将其设为异步函数。比如,同样的 fetchData 可以使用如下代码测试:
test('the data is peanut butter', () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
expect.assertions(1);
try {
await fetchData();
} catch(e) {
expect(e).toMatch('error');
}
});
也可以把 async 和 await 同 .resolves 或 .rejects 结合一起使用。
test('the data is peanut butter', async () => {
await expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
await expect(fetchData()).rejects.toMatch('error');
});
这上面例子中,async 和 await 其实是 promise 的语法糖。
上面四种异步代码形式没有高下之分,可以在一个文件中混用。选择最适合你的形式就好。
参考文档
- Testing Asynchronous Code - jest
- expect.assertions(number) - jest