Jest.js 用承诺开玩笑的假计时器

50pmv0ei  于 2023-05-11  发布在  Jest
关注(0)|答案(4)|浏览(183)

当使用假计时器和promise的组合时,我在让Jest测试框架(版本23.2.0)很好地工作时遇到了一点麻烦。我哪里做错了?
假设我有以下模块:

// timing.js

export const timeout = ms =>
  new Promise(resolve => {
    setTimeout(resolve, ms)
  })

我的测试文件看起来像:

// timing.test.js

import { timeout } from './timing'

describe('timeout()', () => {
  beforeEach(() => {
    jest.useFakeTimers()
  })

  it('resolves in a given amount of time', () => {
    const spy = jest.fn()

    timeout(100).then(spy)
    expect(spy).not.toHaveBeenCalled()

    jest.advanceTimersByTime(100)
    expect(spy).toHaveBeenCalled()
  })
})

此操作失败,输出如下:

● timeout › resolves in a given amount of time

expect(jest.fn()).toHaveBeenCalled()

Expected mock function to have been called, but it was not called.

  15 |
  16 |     jest.advanceTimersByTime(100)
> 17 |     expect(spy).toHaveBeenCalled()
     |                 ^
  18 |   })
  19 | })
  20 |

  at Object.<anonymous> (src/timing.test.js:17:17)

但是,如果我删除promise:

// timing.js
export const timeout = ms => ({
  then: resolve => {
    setTimeout(resolve, ms)
  }
})

。。。测试就会通过

timeout
  ✓ resolves in a given amount of time (5ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.304s

更新

虽然这不是最优雅的解决方案,但我目前正在使用下面的测试。它工作,但我仍然好奇为什么原来的一个没有

import { timeout } from './timing'

describe('timeout', () => {
  it('resolves in a given amount of time', done => {
    setTimeout(() => done(new Error('it didn\'t resolve or took longer than expected')), 10)
    return timeout(9).then(done)
  })
})
dkqlctbz

dkqlctbz1#

目前最好的替代方案是使用异步版本的假定时器。所以你会

await clock.tickAsync(1000); // doesn't wait 1000ms but is async

而不是调用clock.tick。请参阅the answer below了解更多详情。

暂不支持

你没有做错什么-它现在不工作-抱歉。在我们这一方开始工作之前,必须发生以下事情:

  • Jest需要合并正在进行的工作,以合并lolex作为他们的假计时器实现在这里https://github.com/facebook/jest/pull/5171
  • Lolex需要通过Promise来支持pumping--我们已经在最近的Node.js合作者峰会上与V8团队讨论过这个问题。这将公开一个钩子,我们将使用它来允许做类似advanceTimeByTime(100)的事情,并使其与promise一起工作。

问题是.then(spy)只会在以后被调用。
由于我们是志愿者,这些事情没有具体的时间轴。我希望SimenB在未来的2-3个月内完成合并,我将在下个月与V8团队跟进。

你现在能做什么

你可以编写一个异步测试:

// note this is an async function now
it('resolves in a given amount of time', async () => {
  // this is in a promise.reoslve.then to not 'lock' on the await
  Promise.resolve().then(() => jest.advanceTimersByTime(100));
  await timeout(100);
});

如果有其他需要等待的内容,您可以在超时后添加期望。

toe95027

toe950272#

jest@26.0.0开始,您可以在两种不同的伪计时器实现之间进行选择。
我发现jest.useFakeTimers('legacy')可以使用the flushPromises workaround与Promises一起工作,但不能与Date一起工作,而jest.useFakeTimers('modern')可以与Date一起工作,但不能与Promises一起工作,因为await flushPromises()永远不会解析。
我发现最好的解决方案是使用@sinonjs/fake-timers,因为它可以同时使用Promises和Date,而没有任何变通方法或黑客:

import FakeTimers from "@sinonjs/fake-timers";

// Before tests:
const clock = FakeTimers.install();

// In tests:
await clock.tickAsync(100);

// After tests:
clock.uninstall();
mqxuamgl

mqxuamgl3#

在我的例子中,计时器回调调用了其他异步函数,所以其他解决方案对我不起作用。我最终发现,通过手动确保promise队列为空,所有的异步代码都将完成运行,我可以让测试工作:

function flushPromises() {
  // Wait for promises running in the non-async timer callback to complete.
  // From https://stackoverflow.com/a/58716087/308237
  return new Promise(resolve => setImmediate(resolve));
}

test('example', async () => {
  jest.useFakeTimers();

  example_function_to_set_a_timer();

  // Wait for one virtual second
  jest.advanceTimersByTime(1000);

  // Wait for any async functions to finish running
  await flushPromises();

  // Continue with tests as normal
  expect(...);
});
v2g6jxz6

v2g6jxz64#

我想我会添加一个更新的答案,只依赖于Jest,因为在2023年3月6日,Jest在29.5版本中公开了异步API!
这意味着Ben提到使用@sinonjs/fake-timerstickAsync(ms)的答案现在可以直接在Jest中使用:

await jest.advanceTimersByTimeAsync(6000)

相关问题