javascript Jest:Timer 和 Promise 效果不佳。(setTimeout 和异步函数)

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/52177631/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-29 09:51:53  来源:igfitidea点击:

Jest: Timer and Promise don't work well. (setTimeout and async function)

javascripttestingjestjs

提问by GutenYe

Any ideas on this code

关于此代码的任何想法

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)
}

```

``

Failed with

失败

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

However, If I remove awaitfrom LINE-A, the test passes.

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

Does Promise and Timer not work well?

Promise 和 Timer 不能正常工作吗?

I think the reason maybe jest is waiting for second promise to resolve.

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

回答by Brian Adams

Yes, you're on the right track.

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



What happens

怎么了

await simpleTimer(callback)will wait for the Promise returned by simpleTimer()to resolve so callback()gets called the first time and setTimeout()also gets called. jest.useFakeTimers()replaced setTimeout()with a mockso the mock records that it was called with [ () => { simpleTimer(callback) }, 1000 ].

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

jest.advanceTimersByTime(8000)runs () => { simpleTimer(callback) }(since 1000 < 8000) which calls setTimer(callback)which calls callback()the second time and returns the Promise created by await. setTimeout()does not run a second time since the rest of setTimer(callback)is queued in the PromiseJobsqueueand has not had a chance to run.

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

expect(callback).toHaveBeenCalledTimes(9)fails reporting that callback()was only called twice.

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



Additional Information

附加信息

This is a good question. It draws attention to some unique characteristics of JavaScript and how it works under the hood.

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

Message Queue

消息队列

JavaScript uses a message queue. Each message is run to completionbefore the runtime returns to the queue to retrieve the next message. Functions like setTimeout()add messages to the queue.

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

Job Queues

作业队列

ES6 introduces Job Queuesand one of the required job queues is PromiseJobswhich handles "Jobs that are responses to the settlement of a Promise". Any jobs in this queue run after the current message completes and before the next message begins. then()queues a job in PromiseJobswhen the Promise it is called on resolves.

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

async / await

异步/等待

async / awaitis just syntactic sugar over promises and generators. asyncalways returns a Promise and awaitessentially wraps the rest of the function in a thencallback attached to the Promise it is given.

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

Timer Mocks

定时器模拟

Timer Mockswork by replacing functions like setTimeout()with mockswhen jest.useFakeTimers()is called. These mocks record the arguments they were called with. Then when jest.advanceTimersByTime()is called a loop runs that synchronously calls any callbacks that would have been scheduled in the elapsed time, including any that get added while running the callbacks.

定时器嘲笑的工作更换喜欢的功能setTimeout()与嘲笑的时候jest.useFakeTimers()被调用。这些模拟记录了它们被调用的参数。然后,当jest.advanceTimersByTime()被调用时,循环运行同步调用在经过的时间中安排的任何回调,包括在运行回调时添加的任何回调。

In other words, setTimeout()normally queues messages that must wait until the current message completes before they can run. Timer Mocks allow the callbacks to be run synchronously within the current message.

换句话说,setTimeout()通常将必须等待当前消息完成后才能运行的消息排队。Timer Mocks 允许回调在当前消息中同步运行。

Here is an example that demonstrates the above information:

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

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' ]);
});


How to get Timer Mocks and Promises to play nice

如何让 Timer Mocks 和 Promises 发挥出色

Timer Mocks will execute the callbacks synchronously, but those callbacks may cause jobs to be queued in PromiseJobs.

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

Fortunately it is actually quite easy to let all pending jobs in PromiseJobsrun within an asynctest, all you need to do is call await Promise.resolve(). This will essentially queue the remainder of the test at the end of the PromiseJobsqueue and let everything already in the queue run first.

幸运的是,让所有挂起的作业在测试中PromiseJobs运行实际上很容易async,您需要做的就是调用await Promise.resolve(). 这基本上会将测试的其余部分排在队列的末尾,PromiseJobs并让队列中的所有内容首先运行。

With that in mind, here is a working version of the test:

考虑到这一点,这里是测试的工作版本:

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
});

回答by Molten Ice

Brian Adams' answer is spot on.

布赖恩亚当斯的回答是正确的。

But calling await Promise.resolve()only seems to resolve one pending promise.

但是调用await Promise.resolve()似乎只能解决一个未决的承诺。

In the real world, testing functions having multiple asynchronous calls would be painful if we have to call this expression over and over again per iteration.

在现实世界中,如果我们必须在每次迭代中一遍又一遍地调用这个表达式,那么测试具有多个异步调用的函数会很痛苦。

Instead, if your function has multiple awaits it's easier to use jwbay's response:

相反,如果您的函数有多个awaits,则使用jwbay 的响应会更容易:

  1. Create this function somewhere
    function flushPromises() {
      return new Promise(resolve => setImmediate(resolve));
    }
    
  2. Now call await flushPromises()wherever you would have otherwise called multiple await Promise.resolve()s
  1. 在某处创建这个函数
    function flushPromises() {
      return new Promise(resolve => setImmediate(resolve));
    }
    
  2. 现在调用await flushPromises()您本来会调用多个await Promise.resolve()s 的任何地方

回答by nemo

There is a use case I just couldn't find a solution:

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

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)
      })
    })()
  })
}

And the test looks like:

测试看起来像:

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

Basically the action won't resolve unless the timer advances. Feels like a circular dependency here: promise need timer to advance to resolve, fake timer need promise to resolve to advance.

除非计时器提前,否则基本上该操作不会解决。感觉这里是一个循环依赖:promise 需要定时器来提前解决,假定时器需要承诺来解决才能提前。