Jest 002: 测试异步代码

521 阅读3分钟

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

另外,可以在测试中使用 asyncawait。在 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');
    }
});

也可以把 asyncawait.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');
});

这上面例子中,asyncawait 其实是 promise 的语法糖。

上面四种异步代码形式没有高下之分,可以在一个文件中混用。选择最适合你的形式就好。

参考文档

  1. Testing Asynchronous Code - jest
  2. expect.assertions(number) - jest