Jest中setTimeout回调内ES6类方法Mock不生效的原因及解决方案
根因解析
该现象的核心诱因与setTimeout的事件循环特性直接相关,具体有两点:
- 测试执行时序错位:空的测试用例没有任何等待逻辑,Jest在执行完同步代码、清空所有微任务队列后就会判定测试结束。但
setTimeout属于宏任务,优先级在微任务之后——代码中setTimeout(..., 0)的回调,会在Jest已经退出测试流程后才被事件循环执行,此时测试环境已经完成上下文清理,自然无法拿到预期的Mock返回值。
观察到的“同步链路中的.then()能拿到Mock”,本质是因为.then()的回调属于微任务,会在当前同步栈执行完成后立刻触发,仍处于Jest的测试等待窗口期内,因此可以正常获取Mock值。 - Mock实现写法的缺陷:当前的
jest.mock没有保留类的原型链路,搭配beforeEach中的mockClear()如果使用不当,会破坏之前定义的Mock返回逻辑,导致延迟回调中拿到的实例没有绑定预期的Mock方法。
修正方案
按照以下步骤调整,即可覆盖包含延迟回调的类方法Mock场景:
- 接入Jest伪定时器接管所有
setTimeout/setInterval的执行时序,无需等待真实时间流逝,即可确保定时器回调在测试断言前被执行 - 保留原有ES6类的Mock结构,避免使用会破坏原型链的工厂函数写法
- 测试断言阶段手动推进所有定时器执行、等待微任务队列清空,确保所有业务逻辑完成后再执行校验
修正后的测试代码参考:
import Util from '../src/test/util_test' import Test from '../src/test/test' import { mocked } from 'ts-jest/utils' // 启用Jest伪定时器 jest.useFakeTimers(); jest.mock('../src/test/util_test', () => { return jest.fn().mockImplementation(() => ({ getMemberAccounts: jest.fn().mockResolvedValue({ accounts: [ { id: '0zh8ap6luxoijne8qlfu1sg', name: 'test', officeNumber: '23', emailAddress: 'string@gmail' } ] }) })); }); describe('Test', () => { let testCase: Test; beforeEach(() => { // 仅清空Mock调用记录,不重置Mock实现 mocked(Util).mockClear(); // 清空上一个测试遗留的定时器队列,避免交叉污染 jest.clearAllTimers(); testCase = new Test(); }) test('test-case', async () => { // 快进执行所有待触发的定时器回调 jest.runAllTimers(); // 等待所有Promise微任务执行完成 await Promise.resolve(); // 此处可正常获取到Mock的返回值完成断言 const utilInstance = mocked(Util).mock.instances[0]; expect(utilInstance.getMemberAccounts).toHaveBeenCalledTimes(2); }) afterAll(() => { // 测试执行完成后切回真实定时器,避免影响其他测试文件 jest.useRealTimers(); }) });
注意事项
- 不要用
mockReset()替换mockClear(),前者会完全清空定义的mockResolvedValue等实现逻辑,后者仅清除历史调用记录和实例栈,不会破坏Mock预设行为 - 如果不想引入伪定时器,也可以在测试中手动等待一个宏任务周期,比如
await new Promise(resolve => setTimeout(resolve, 0)),但这种方式执行效率低于伪定时器,容易受运行环境的随机延迟影响 - 挂载在实例上的类方法(比如示例中的
getMemberAccounts为箭头函数属性),不需要额外挂载到类原型上,只要Mock工厂返回的实例结构与原类保持一致即可正常Mock
内容的提问来源于stack exchange,提问作者Nguyễn Hoàng Hưng
相关产品推荐
相关产品推荐

