Testing Asynchronous Code
Зачастую JavaScript код выполняется асинхронно. При работе с асинхронным кодом, Jest нужно знать когда тестируемый код завершен, до того, как он сможет перейти к следующему тесту. В Jest этого можно добиться несколькими способами.
Обратные вызовы
Наиболее распространенный шаблон работы с асинхронным кодом это обратные вызовы.
Представим, что у вас есть функция fetchData(callback)
, которая получает некоторые данные, и вызывает callback(data)
когда она будет завершена. И вы хотите проверить, что возвращаемые данные это строка 'peanut butter'
.
По умолчанию Jest тесты завершаются, как только они достигают конца их исполнения. Это значит, что этот тест не будет работать как предполагается:
// Don't do this!
test('the data is peanut butter', () => {
function callback(data) {
expect(data).toBe('peanut butter');
}
fetchData(callback);
});
Проблема в том, что тест завершится, как только завершится выполнение fetchData
, прежде чем будет вызван callback
.
Существует альтернативная форма 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);
});
If done()
is never called, the test will fail (with timeout error), which is what you want to happen.
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)
.
Промисы
Если в вашем коде используются промисы, есть более простой способ обработки асинхронных тестов. Возвращайте промис в своем тесте, и Jest будет ждать resolve
— успешного завершения промиса. If the promise is rejected, the test will automatically fail.
For example, let's say that fetchData
, instead of using a callback, 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');
});
});
Обязательно убедитесь, что вы возвращаете промис — если забыть про этот return
, то тест завершится еще до того как успешно завершится промис, вернувшийся из fetchData
, и у then() появится возможность выполнить обратный вызов.
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(e => expect(e).toMatch('error'));
});
.resolves
/ .rejects
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
, и у then() появится возможность выполнить обратный вызов.
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');
});
Async/Await
Alternatively, you can use async
and await
in your tests. Чтобы написать асинхронный тест, просто используйте async
перед определением функции передаваемой в 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 (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.toThrow('error');
});
В этих случаях, async
и await
удобный синтаксический сахар для той же самой логики, что использовалась с примерами на промисах.
Ни одна их этих форм не обладает серьезными преимуществами перед остальными, и вы можете смешивать и сопоставлять их во всем своем коде или даже в рамках одного файла. Все зависит только от того, в каком стиле вам легче писать тесты.