开玩笑假计时器与承诺

问题描述 投票:0回答:5

在使用假计时器和承诺的组合时,我在让 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)

但是,如果我删除承诺:

// 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)
  })
})
javascript promise jestjs
5个回答
38
投票

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

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

而不是打电话给

clock.tick
。请参阅下面的答案了解更多详情。

暂时不支持

您没有做错任何事 - 目前不起作用 - 抱歉。在此之前,必须先完成以下事情:

  • Jest 需要合并正在进行的工作,以将 lolex 合并为他们的假计时器实现 https://github.com/facebook/jest/pull/5171
  • Lolex 需要支持通过承诺进行泵送 - 我们在最近的 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);
});

如果还有其他想要等待的事情,您可以在超时后添加期望。


16
投票

[email protected]
开始,您可以在两种不同的假定时器实现之间进行选择。

我发现

jest.useFakeTimers('legacy')
可以使用
flushPromises
解决方法
与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();

10
投票

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

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

5
投票

2023 年 3 月 6 日 Jest 在版本 29.5 中添加了异步计时器 API!

Jest 在内部依赖于

Sinon 项目的 fake-timers

,因此它实际上已经访问该功能已有五年了,但需要有人站出来并完成工作以将其公开在 Jest 实例上。直到现在才发生这种情况。

这意味着 Ben 的答案(从 2018 年开始)提到使用

@sinonjs/fake-timers

tickAsync(ms)
 现在可以直接在 Jest 中使用:

await jest.advanceTimersByTimeAsync(6000)
    

0
投票
我刚刚遇到了这个;我设法用这个修复它:

https://jestjs.io/docs/jest-object#jestrunalltimersasync

beforeAll(() => { jest.useFakeTimers(); jest.runAllTimersAsync(); // <-- }); afterAll(() => { jest.useRealTimers(); });
对我来说,嵌套在 Promise 中的 

setTimeout

 会立即解析,而不是等待指定的超时值。

© www.soinside.com 2019 - 2024. All rights reserved.