首页 > 解决方案 > 笑话:Timer 和 Promise 不能很好地工作。(setTimeout 和异步函数)

问题描述

关于此代码的任何想法

jest.useFakeTimers() 

it('simpleTimer', async () => {
  async function simpleTimer(callback) {
    await callback()    // LINE-A without await here, test works as expected.
    setTimeout(() => {
      simpleTimer(callback)
    }, 1000)
  }

  const callback = jest.fn()
  await simpleTimer(callback)
  jest.advanceTimersByTime(8000)
  expect(callback).toHaveBeenCalledTimes(9)
}

```

失败了

Expected mock function to have been called nine times, but it was called two times.

但是,如果我await从 LINE-A 中删除,则测试通过。

Promise 和 Timer 不能很好地工作吗?

我认为可能开玩笑的原因是等待第二个承诺解决。

标签: javascripttestingjestjs

解决方案


是的,你在正确的轨道上。


发生什么了

await simpleTimer(callback)将等待返回的 PromisesimpleTimer()解决,因此callback()第一次被调用并且setTimeout()也被调用。 jest.useFakeTimers() 替换setTimeout()为模拟,因此模拟记录了它被调用的[ () => { simpleTimer(callback) }, 1000 ].

jest.advanceTimersByTime(8000)运行() => { simpleTimer(callback) }(从 1000 < 8000 开始)调用setTimer(callback)whichcallback()第二次调用并返回由await. setTimeout()不会第二次运行,因为其余的setTimer(callback) 都在队列中PromiseJobs排队并且没有机会运行。

expect(callback).toHaveBeenCalledTimes(9)callback()只被调用两次的报告失败。


附加信息

这是一个很好的问题。它引起了人们对 JavaScript 的一些独特特性以及它在底层如何工作的关注。

消息队列

JavaScript 使用消息队列。在运行时返回队列以检索下一条消息之前,每条消息都会运行完成。setTimeout() 将消息添加到队列等功能。

作业队列

ES6 引入Job Queues并且所需的作业队列之一是PromiseJobs处理“作为对 Promise 解决的响应的作业”。此队列中的任何作业都在当前消息完成之后和下一条消息开始之前运行。 当调用它的 Promise 解决时,将then()作业排队。PromiseJobs

异步/等待

async / await 只是 promises 和 generators 的语法糖async总是返回一个 Promise 并且await本质上将函数的其余部分包装在then附加到给定 Promise 的回调中。

计时器模拟

Timer Mocks的工作原理是在调用时用 mocks替换函数setTimeout()jest.useFakeTimers()。这些模拟记录了它们被调用的参数。然后,当jest.advanceTimersByTime()被调用时,将运行一个循环,该循环将同步调用在经过的时间内安排的任何回调,包括在运行回调时添加的任何回调。

换句话说,setTimeout()通常将必须等到当前消息完成才能运行的消息排队。Timer 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 和 Promise 发挥得很好

Timer Mocks 将同步执行回调,但这些回调可能会导致作业在PromiseJobs.

PromiseJobs幸运的是,让所有待处理的作业在测试中运行实际上很容易async,您需要做的就是调用await Promise.resolve(). 这实际上会将剩余的测试排在队列的末尾,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
});

推荐阅读