测试异步代码
在 JavaScript 中,代码异步运行很常见。当你的代码异步运行时,Jest 需要知道它正在测试的代码何时完成,然后才能继续进行另一个测试。Jest 有几种方法来处理这个问题。
¥It's common in JavaScript for code to run asynchronously. When you have code that runs asynchronously, Jest needs to know when the code it is testing has completed, before it can move on to another test. Jest has several ways to handle this.
Promise
从测试返回一个 promise,Jest 将等待该 promise 解决。如果 promise 被拒绝,测试就会失败。
¥Return a promise from your test, and Jest will wait for that promise to resolve. If the promise is rejected, the test will fail.
例如,假设 fetchData
返回一个应解析为字符串 'peanut butter'
的 Promise。我们可以用以下方法测试它:
¥For example, let's say that fetchData
returns a promise that is supposed to resolve to the string 'peanut butter'
. We could test it with:
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
异步/等待
¥Async/Await
或者,你可以在测试中使用 async
和 await
。要编写异步测试,请在传递给 test
的函数前面使用 async
关键字。例如,可以使用以下命令测试相同的 fetchData
场景:
¥Alternatively, you can use async
and await
in your tests. To write an async test, use the async
keyword in front of the function passed to test
. For example, the same fetchData
scenario can be tested with:
test('the data is peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
expect.assertions(1);
try {
await fetchData();
} catch (error) {
expect(error).toMatch('error');
}
});
你可以将 async
和 await
与 .resolves
或 .rejects
组合起来。
¥You can combine async
and await
with .resolves
or .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 示例使用的逻辑相同。
¥In these cases, async
and await
are effectively syntactic sugar for the same logic as the promises example uses.
请务必返回(或 await
)promise - 如果省略 return
/await
语句,你的测试将在 fetchData
返回的 promise 解析或拒绝之前完成。
¥Be sure to return (or await
) the promise - if you omit the return
/await
statement, your test will complete before the promise returned from fetchData
resolves or rejects.
如果你预计 Promise 会被拒绝,请使用 .catch
方法。确保添加 expect.assertions
以验证是否调用了一定数量的断言。否则,履行的 promise 就不会通过测试。
¥If you expect a promise to be rejected, use the .catch
method. Make sure to add expect.assertions
to verify that a certain number of assertions are called. Otherwise, a fulfilled promise would not fail the test.
test('the fetch fails with an error', () => {
expect.assertions(1);
return fetchData().catch(error => expect(error).toMatch('error'));
});
回调
¥Callbacks
如果不使用 Promise,则可以使用回调。例如,假设 fetchData
不返回 promise,而是期望回调,即获取一些数据并在完成时调用 callback(null, data)
。你想测试这个返回的数据是否是字符串 'peanut butter'
。
¥If you don't use promises, you can use callbacks. For example, let's say that fetchData
, instead of returning a promise, expects a callback, i.e. fetches some data and calls callback(null, data)
when it is complete. You want to test that this returned data is the string 'peanut butter'
.
默认情况下,Jest 测试在执行结束后即完成。这意味着此测试将无法按预期进行:
¥By default, Jest tests complete once they reach the end of their execution. That means this test will not work as intended:
// Don't do this!
test('the data is peanut butter', () => {
function callback(error, data) {
if (error) {
throw error;
}
expect(data).toBe('peanut butter');
}
fetchData(callback);
});
问题是测试将在 fetchData
完成后立即完成,然后再调用回调。
¥The problem is that the test will complete as soon as fetchData
completes, before ever calling the callback.
test
的另一种形式可以解决这个问题。不要将测试放在带有空参数的函数中,而是使用名为 done
的单个参数。Jest 将等到 done
回调被调用后才完成测试。
¥There is an alternate form of test
that fixes this. Instead of putting the test in a function with an empty argument, use a single argument called done
. Jest will wait until the done
callback is called before finishing the test.
test('the data is peanut butter', done => {
function callback(error, data) {
if (error) {
done(error);
return;
}
try {
expect(data).toBe('peanut butter');
done();
} catch (error) {
done(error);
}
}
fetchData(callback);
});
如果 done()
从未被调用,测试将失败(出现超时错误),这正是你希望发生的情况。
¥If done()
is never called, the test will fail (with timeout error), which is what you want to happen.
如果 expect
语句失败,则会抛出错误并且不会调用 done()
。如果我们想在测试日志中查看失败的原因,我们必须将 expect
封装在 try
块中,并将 catch
块中的错误传递给 done
。否则,我们最终会遇到不透明的超时错误,该错误不会显示 expect(data)
收到的值。
¥If the expect
statement fails, it throws an error and done()
is not called. If we want to see in the test log why it failed, we have to wrap expect
in a try
block and pass the error in the catch
block to done
. Otherwise, we end up with an opaque timeout error that doesn't show what value was received by expect(data)
.
如果相同的测试函数传递 done()
回调并返回 promise,Jest 将抛出错误。这样做是为了避免测试中出现内存泄漏。
¥Jest will throw an error, if the same test function is passed a done()
callback and returns a promise. This is done as a precaution to avoid memory leaks in your tests.
.resolves
/ .rejects
你还可以在你的期望语句中使用 .resolves
匹配器,Jest 将等待该 promise 解决。如果 promise 被拒绝,测试将自动失败。
¥You can also use the .resolves
matcher in your expect statement, and Jest will wait for that promise to resolve. If the promise is rejected, the test will automatically fail.
test('the data is peanut butter', () => {
return expect(fetchData()).resolves.toBe('peanut butter');
});
请务必返回断言 - 如果省略此 return
语句,则测试将在从 fetchData
返回的 Promise 得到解决之前完成,然后()有机会执行回调。
¥Be sure to return the assertion—if you omit this return
statement, your test will complete before the promise returned from fetchData
is resolved and then() has a chance to execute the callback.
如果你预计 Promise 会被拒绝,请使用 .rejects
匹配器。它的工作原理与 .resolves
匹配器类似。如果 promise 兑现,测试将自动失败。
¥If you expect a promise to be rejected, use the .rejects
matcher. It works analogically to the .resolves
matcher. If the promise is fulfilled, the test will automatically fail.
test('the fetch fails with an error', () => {
return expect(fetchData()).rejects.toMatch('error');
});
这些形式都不是特别优于其他形式,你可以在代码库甚至单个文件中混合和匹配它们。这仅取决于你认为哪种风格可以让你的测试更简单。
¥None of these forms is particularly superior to the others, and you can mix and match them across a codebase or even in a single file. It just depends on which style you feel makes your tests simpler.