【问题标题】:Jest "Async callback was not invoked within the 5000 ms timeout" with monkey-patched `test` and useFakeTimers用猴子修补的`test`和useFakeTimers开玩笑“在5000毫秒超时内未调用异步回调”
【发布时间】:2021-07-31 09:18:52
【问题描述】:

这个设置非常具体,但我在网上找不到任何类似的资源,所以我在这里发帖以防对任何人有帮助。


关于 Jest 和 Async callback was not invoked 的问题有很多,但我没有发现任何问题的根本问题都围绕着 jest.useFakeTimers() 的使用。使用假计时器时,我的函数应该不会花时间执行,但由于某种原因,Jest 挂起。

我正在使用 Jest 26,因此我手动指定使用 modern 计时器。

这是演示问题的完整代码 sn-p。

jest.useFakeTimers('modern')
let setTimeoutSpy = jest.spyOn(global, 'setTimeout')

async function retryThrowable(
  fn,
  maxRetries = 5,
  currentAttempt = 0
) {
  try {
    return await fn()
  } catch (e) {
    if (currentAttempt < maxRetries) {
      setTimeout(
        () => retryThrowable(fn, maxRetries, currentAttempt + 1),
        1 * Math.pow(1, currentAttempt)
      )
    }
    throw e
  }
}

describe('retryThrowable', () => {
  const fnErr = jest.fn(async () => { throw new Error('err') })

  it('retries `maxRetries` times if result is Err', async () => {
    jest.clearAllMocks()
    const maxRetries = 5

    await expect(retryThrowable(() => fnErr(), maxRetries)).rejects.toThrow('err')

    for (let _ in Array(maxRetries).fill(0)) {
      jest.runAllTimers()
      await Promise.resolve() // https://stackoverflow.com/a/52196951/3991555
    }

    expect(setTimeoutSpy).toHaveBeenCalledTimes(maxRetries)
  })
})

完整的错误信息是

Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error: Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.

      at mapper (../../node_modules/jest-jasmine2/build/queueRunner.js:27:45)

任何想法将不胜感激


编辑1:我试过--detectOpenHandles,但没有提供新信息


edit 2:我刚刚在一个新项目中尝试了上面的代码 sn-p 并意识到它通过就好了。所以这个问题必须在我的 Jest 配置中的其他地方。当我确定根本原因时,我会回答我自己的问题

【问题讨论】:

    标签: javascript asynchronous jestjs


    【解决方案1】:

    我的问题最终出在我的笑话配置中。

    我们直接针对内存中的数据库执行测试,为了保持测试干净,我们将每个测试包装在数据库事务中。 Jest 不像许多其他测试运行器那样提供原生的aroundEach 钩子,所以我们通过猴子修补全局testit 函数来实现这一点,这样我们就可以在事务中执行测试回调。不确定这是否重要,但要明确的是,我们使用 Sequelize 作为我们的 ORM 和事务。

    我正在执行的测试(如上所示)递归调用 setTimeout 并带有一个引发错误/拒绝 Promise 的函数。 Sequelize transactions 显然不喜欢未处理的拒绝,这导致测试挂起。我一直无法找到测试挂起的根本原因;事务成功回滚,所有测试预期都运行了,但由于某种原因,测试从未退出。

    解决方案 #1(不是很好)

    我的第一个解决方案并不漂亮,但它很实用。我只是用不使用猴子补丁的test 的变体扩展了 Jest test 函数。

    // jest.setup.ts
    declare namespace jest {
      interface It {
        noDb: (name: string, fn?: ProvidesCallback, timeout?: number) => void
      }
    }
    
    it.noDb = it
    
    // jest.config.js
    module.exports = {
      // ...
      setupFilesAfterEnv: [
        './jest.setup.ts', // <-- inject `it.noDb` method
        './jest.mokey-patch.ts', // <-- monkey-patching
      ],
    }
    

    然后,我从 OP 修改了测试以调用这个新函数

    it.noDb('retries `maxRetries` times if result is Err', ...
    

    有关此扩展如何以及为何工作的更多详细信息,请参阅this blog post

    解决方案 #2(更好)

    在搞砸了更多之后,我意识到根本问题是在主线程中发生了未处理的 Promise 拒绝。我不知道为什么这与 Sequelize Transactions 冲突,但我只想说这是不好的做法。

    我能够完全避免这个问题,以及任何奇怪的 Jest 扩展,只需将方法修复为仅在第一次调用时抛出。这样,我们可以在调用 retryThrowable 时处理错误,但不会在后续调用中抛出错误。

      // ...
      try {
        return await fn()
      } catch (e) {
        if (currentAttempt < maxRetries) {
          setTimeout(
            () => retryThrowable(fn, maxRetries, currentAttempt + 1),
            1 * Math.pow(1, currentAttempt)
          )
        }
    
        // ? this is the new part
        if (currentAttempt === 0) {
          throw e
        }
      }
      // ...
    

    【讨论】:

      猜你喜欢
      • 2020-05-13
      • 2020-12-09
      • 2019-03-11
      • 2021-05-13
      • 2018-09-18
      • 2018-09-11
      • 2020-01-15
      • 2020-01-07
      相关资源
      最近更新 更多