js单元测试中用fake timers模拟debounce需启用现代假定时器、手动推进时间并验证回调执行或取消:jest用jest.usefaketimers({legacyfaketimers:false}),调用debounced后用jest.advancetimersbytime()控制延迟,确保只执行最后一次调用。

在 JS 单元测试中,用 fake timers 模拟防抖函数(debounce)的行为,核心是控制时间流逝、触发延迟执行,并验证回调是否按预期被调用或取消。主流测试框架如 Jest 和 Vitest 都原生支持 fake timers,关键在于正确启用、推进时间、清理状态。
启用 fake timers 并封装 debounce
确保测试环境使用 fake timers 替代真实 `setTimeout`/`clearTimeout`。Jest 中推荐用 modern timers(基于 `@sinonjs/fake-timers`):
// 示例:Jest 配置(推荐在 test 文件顶部或 setup 文件中)
jest.useFakeTimers({ legacyFakeTimers: false });
防抖函数本身应返回一个可清除的函数(便于测试中手动 cancel),例如:
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
验证「延迟执行」和「被取消」两种行为
通过 jest.advanceTimersByTime(ms) 或 jest.runAllTimers() 主动推进时间,观察回调是否触发:
- 调用防抖函数后,不推进时间 → 回调不应执行(断言未被调用)
- 推进 少于 delay 的时间 → 回调仍不应执行
- 推进 ≥ delay 的时间 → 回调应执行一次(且仅一次)
- 多次快速调用防抖函数 → 只有最后一次调用会在 delay 后执行(中间的被 clearTimeout 取消)
完整 Jest 测试示例
describe('debounce', () => {
beforeEach(() => {
jest.useFakeTimers({ legacyFakeTimers: false });
});
afterEach(() => {
jest.clearAllTimers();
});
it('should call the function after delay', () => {
const fn = jest.fn();
const debounced = debounce(fn, 100);
debounced();
expect(fn).not.toHaveBeenCalled();
jest.advanceTimersByTime(100);
expect(fn).toHaveBeenCalledTimes(1);
});
it('should cancel previous calls when called again', () => {
const fn = jest.fn();
const debounced = debounce(fn, 100);
debounced(); // 第一次
jest.advanceTimersByTime(50);
debounced(); // 第二次(取消第一次)
jest.advanceTimersByTime(100); // 等待第二次的 delay
expect(fn).toHaveBeenCalledTimes(1); // 只执行第二次
});
});
注意点与常见坑
- 务必在
beforeEach启用 fake timers,在afterEach清理(jest.clearAllTimers()),避免测试间干扰 - 不要混用
jest.useFakeTimers('modern')和旧版jest.useFakeTimers(),后者已废弃 - Vitest 用户需用
vi.useFakeTimers()+vi.advanceTimersByTime(),API 类似但前缀是vi - 如果 debounce 内部用了
requestIdleCallback或Promise.then,fake timers 不会自动 mock 它们,需额外处理
不复杂但容易忽略的是:fake timers 不会自动“等待”,必须显式推进时间;而 debounce 的本质就是“等一等再执行”,所以测试里所有“等”都得你亲手推进。











