Ответ 1
Да, вы на правильном пути.
Что просходит
await simpleTimer(callback)
будет ожидать разрешения Promise, возвращаемого simpleTimer()
поэтому callback()
simpleTimer()
в первый раз, а setTimeout()
также вызывается. jest.useFakeTimers()
заменил setTimeout()
на макет, чтобы макет записывал, что он вызывался с помощью [() => { simpleTimer(callback) }, 1000 ]
.
jest.advanceTimersByTime(8000)
() => { simpleTimer(callback) }
(начиная с 1000 <8000), который вызывает setTimer(callback)
который вызывает callback()
второй раз и возвращает Promise, созданный await
. setTimeout()
не запускается второй раз, так как остальная часть setTimer(callback)
находится в очереди в очереди PromiseJobs
и не имеет возможности для запуска.
expect(callback).toHaveBeenCalledTimes(9)
не удается сообщить о том, что callback()
был вызван только дважды.
Дополнительная информация
Это хороший вопрос. Он привлекает внимание к некоторым уникальным характеристикам JavaScript и тому, как он работает под капотом.
Очередь сообщений
JavaScript использует очередь сообщений. Каждое сообщение выполняется до завершения, прежде чем среда выполнения возвращается в очередь для получения следующего сообщения. Такие функции, как setTimeout()
добавляют сообщения в очередь.
Вакансии
ES6 представляет Job Queues
и одна из необходимых очередей заданий - PromiseJobs
которая обрабатывает "Задания, которые являются ответами на урегулирование Обещания". Все задания в этой очереди выполняются после завершения текущего сообщения и до начала следующего сообщения. then()
PromiseJobs
задание в PromiseJobs
в PromiseJobs
когда Promise, для которого оно вызывается, разрешается.
асинхронный/ожидание
async/await
- это просто синтаксический сахар над обещаниями и генераторами. async
всегда возвращает обещание и await
, по существу оборачивает остальную часть функции в then
обратном вызове, прикрепленные к Promise оно дано.
Таймер издевается
Mock-таймеры работают, заменяя функции вроде setTimeout()
на jest.useFakeTimers()
когда jest.useFakeTimers()
. Эти издевательства записывают аргументы, с которыми они были вызваны. Затем, когда jest.advanceTimersByTime()
запускается цикл, который синхронно вызывает любые обратные вызовы, которые были бы запланированы за истекшее время, включая любые, которые добавляются во время выполнения обратных вызовов.
Другими словами, setTimeout()
обычно ставит в очередь сообщения, которые должны дождаться завершения текущего сообщения, прежде чем они смогут работать. Таймер Mocks позволяет выполнять обратные вызовы синхронно в текущем сообщении.
Вот пример, который демонстрирует вышеуказанную информацию:
jest.useFakeTimers();
test('execution order', async () => {
const order = [];
order.push('1');
setTimeout(() => { order.push('6'); }, 0);
const promise = new Promise(resolve => {
order.push('2');
resolve();
}).then(() => {
order.push('4');
});
order.push('3');
await promise;
order.push('5');
jest.advanceTimersByTime(0);
expect(order).toEqual([ '1', '2', '3', '4', '5', '6' ]);
});
Как получить Timer Mocks и Promises, чтобы играть красиво
Timer Mocks будет выполнять обратные вызовы синхронно, но эти обратные вызовы могут привести к тому, что задания будут поставлены в очередь в PromiseJobs
.
К счастью, на самом деле довольно просто разрешить выполнение всех ожидающих заданий в PromiseJobs
в рамках async
теста, все, что вам нужно сделать, это вызвать await Promise.resolve()
. Это по существу PromiseJobs
очередь оставшуюся часть теста в конце очереди PromiseJobs
и позволит сначала запустить все, что уже находится в очереди.
Имея это в виду, вот рабочая версия теста:
jest.useFakeTimers()
it('simpleTimer', async () => {
async function simpleTimer(callback) {
await callback();
setTimeout(() => {
simpleTimer(callback);
}, 1000);
}
const callback = jest.fn();
await simpleTimer(callback);
for(let i = 0; i < 8; i++) {
jest.advanceTimersByTime(1000);
await Promise.resolve(); // allow any pending jobs in the PromiseJobs queue to run
}
expect(callback).toHaveBeenCalledTimes(9); // SUCCESS
});