笑话:“计时器”和“承诺”效果不佳。 (setTimeout和异步功能)

时间:2018-09-05 05:25:59

标签: javascript testing jestjs

有关此代码的任何想法

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.

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

Promise和Timer工作不正常吗?

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

4 个答案:

答案 0 :(得分:24)

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


会发生什么

await simpleTimer(callback)将等待simpleTimer()返回的Promise解析,因此callback()会被首次调用,而setTimeout()也将被调用。 jest.useFakeTimers() replaced setTimeout() with a mock,因此该模拟记录记录了它是通过[ () => { simpleTimer(callback) }, 1000 ]调用的。

jest.advanceTimersByTime(8000)运行() => { simpleTimer(callback) }(自1000 <8000开始),后者调用setTimer(callback),后者再次调用callback(),并返回由await创建的Promise。 setTimeout()setTimer(callback) is queued in the PromiseJobs queue的其余部分以来没有第二次运行,并且没有机会运行。

expect(callback).toHaveBeenCalledTimes(9)无法报告callback()仅被两次调用。


其他信息

这是一个好问题。它引起人们对JavaScript的一些独特特征及其在幕后的工作方式的关注。

消息队列

JavaScript使用a message queue。在运行时返回队列以检索下一条消息之前,每条消息都是run to completion。像setTimeout() add messages to the queue之类的功能。

工作队列

ES6 introduces Job Queues,并且所需的作业队列之一是PromiseJobs,该队列处理“作为对Promise结算的响应的作业”。在当前消息完成之后且在下一条消息开始之前,该队列中的所有作业都运行 。  当在解决方案上调用Promise时,then()PromiseJobs中将作业排队。

异步/等待

async / await is just syntactic sugar over promises and generatorsasync总是返回Promise,而await本质上将其余函数包装在附加到给定Promise的then回调中。

计时器模拟

Timer Mocksreplacing functions like setTimeout() with mocks工作,当调用jest.useFakeTimers()时。这些模拟记录了调用它们的参数。然后,在调用jest.advanceTimersByTime()时,将运行一个循环,该循环同步调用在经过的时间内已计划的所有回调,包括在运行这些回调时添加的任何回调。

换句话说,setTimeout()通常将必须等待直到当前消息完成才能运行的消息排队。计时器提示允许回调在当前消息中同步运行。

下面是一个演示上述信息的示例:

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将同步执行回调,但是这些回调可能导致作业在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
});

答案 1 :(得分:1)

布莱恩·亚当斯的答案就在眼前。

但是呼叫await Promise.resolve()似乎只能解决一个未决的诺言。

在现实世界中,如果必须在每次迭代中一次又一次地调用此表达式,则测试具有多个异步调用的函数将很痛苦。

相反,如果您的函数有多个await,则使用jwbay's response会更容易:

  1. 在某处创建此功能
    function flushPromises() {
      return new Promise(resolve => setImmediate(resolve));
    }
    
  2. 现在无论您要呼叫多个await flushPromises()的地方,都致电await Promise.resolve()

答案 2 :(得分:0)

有一个用例我只是找不到解决方案:

function action(){
  return new Promise(function(resolve, reject){
    let poll
    (function run(){
      callAPI().then(function(resp){
        if (resp.completed) {
          resolve(response)
          return
        }
        poll = setTimeout(run, 100)
      })
    })()
  })
}

测试如下:

jest.useFakeTimers()
const promise = action()
// jest.advanceTimersByTime(1000) // this won't work because the timer is not created
await expect(promise).resolves.toEqual(({completed:true})
// jest.advanceTimersByTime(1000) // this won't work either because the promise will never resolve

基本上,除非计时器提前,否则操作不会解决。这里感觉像是循环依赖:promise需要计时器来解决,伪计时器需要promise来解决。

答案 3 :(得分:0)

以上真的很有帮助!对于那些尝试使用 React hooks(!) 执行此操作的人,以下代码对我们有用:

// hook
export const useApi = () => {
  const apis = useCallback(
    async () => {
      await Promise.all([
        new Promise((resolve) => {
          api().then(resolve);
        }),
        new Promise((resolve) => {
          return setTimeout(() => {
            resolve();
          }, 10000);
        }),
      ]);
    },
    [],
  );
  return [apis];
}

// test
import { renderHook, act } from '@testing-library/react-hooks';
function flushPromises() {
  return new Promise((resolve) => setImmediate(resolve))
}

it('tests useApi', async () => {
  jest.useFakeTimers();
  const { result } = renderHook(() => useApi());
  api.mockReturnValue(Promise.resolve());
  await act(async () => {
    const promise = result.current[0]()
    await flushPromises()
    jest.runAllTimers()

    return promise
  })
});