Jest异步测试:如何替代done()实现多回调完成后的断言?
Jest 多异步回调测试解决方案
你的测试用例存在两个核心问题:
done()被提前触发(比如单个回调第一次执行就调用done,或者多个回调中任意一个执行就调用done),导致测试在所有逻辑完成前结束- 同步断言
toHaveBeenCalledTimes()在setTimeout回调执行前就运行,断言结果必然不符合预期
下面是针对性的解决方案:
一、模拟定时器,消除真实等待
Jest提供假定时器能力,可以同步执行所有待处理的异步定时器回调,不用等待真实时间,同时提升测试效率。需要先在测试前后配置假定时器:
beforeEach(() => { // 启用假定时器 jest.useFakeTimers(); }); afterEach(() => { // 测试结束后恢复真实定时器,避免影响其他测试 jest.useRealTimers(); });
二、计数器追踪所有回调完成
针对多回调或多次调用的场景,用计数器记录已执行的回调数量,当所有回调都执行完成后再结束测试。
修复示例1:单个回调被调用两次
it('测试单个回调被执行两次', (done) => { const args = 'some value'; const callback = jest.fn((arg) => { expect(arg).toBe(args); // 当回调被调用第2次时,才调用done结束测试 if (callback.mock.calls.length === 2) { done(); } }); subscribe('eventName', callback); publish('eventName', args); publish('eventName', args); // 同步执行所有待处理的setTimeout回调 jest.runAllTimers(); // 此时回调已执行,可同步断言调用次数 expect(callback).toHaveBeenCalledTimes(2); });
修复示例2:多个回调执行完成
it('测试所有回调在publish后执行', (done) => { const args = 'some value'; const totalCallbacks = 2; let completedCount = 0; // 封装带计数的回调 const createTrackedCallback = (originalCb) => (...cbArgs) => { originalCb(...cbArgs); completedCount++; // 所有回调都执行完成后结束测试 if (completedCount === totalCallbacks) { done(); } }; const callback1 = createTrackedCallback(jest.fn((arg) => { expect(arg).toBe(args); })); const callback2 = createTrackedCallback(jest.fn((arg) => { expect(arg).toBe(args); })); subscribe('eventName', callback1); subscribe('eventName', callback2); publish('eventName', args); // 同步执行定时器回调 jest.runAllTimers(); // 同步断言调用次数 expect(callback1).toHaveBeenCalledTimes(1); expect(callback2).toHaveBeenCalledTimes(1); });
三、用Promise替代done(推荐)
如果不想用done,可以用Promise包装回调逻辑,结合async/await让测试更简洁:
it('测试单个回调被执行两次', async () => { const args = 'some value'; let resolveTriggerCount = 0; let callback; // 创建Promise,当回调执行2次后resolve const callbackDone = new Promise((resolve) => { callback = jest.fn((arg) => { expect(arg).toBe(args); resolveTriggerCount++; if (resolveTriggerCount === 2) { resolve(); } }); subscribe('eventName', callback); }); publish('eventName', args); publish('eventName', args); jest.runAllTimers(); // 等待所有回调执行完成 await callbackDone; // 断言最终调用次数 expect(callback).toHaveBeenCalledTimes(2); });
额外注意点
- 原
publish函数存在笔误:callback(...args)应该改为cb(...args)(forEach循环里的参数是cb) - 测试中要确保
args变量已定义,否则会报错 - 如果需要精确模拟时间(比如等待指定毫秒后执行),可以用
jest.advanceTimersByTime(1000)替代jest.runAllTimers()
内容的提问来源于stack exchange,提问作者scazzy
相关产品推荐
相关产品推荐

