Vitest 测试定时器逻辑需启用 vi.useFakeTimers() 模拟时间,推荐 modern 模式;用 vi.advanceTimeBy(ms) 精确推进时间,配合 vi.waitFor 处理异步回调;测试后必须调用 vi.useRealTimers() 清理。

在 Vitest 中测试涉及定时器的逻辑(比如倒计时、轮询、防抖),不能依赖真实时间流逝,必须用模拟(mock)方式控制时间推进。Vitest 内置了对 setTimeout、setInterval 等全局定时器的高精度模拟支持,无需额外库,关键在于启用并正确使用 vi.useFakeTimers()。
启用 fake timers 并选择合适模式
Vitest 提供两种底层模拟策略:
-
legacy 模式:兼容旧版 Jest 行为,直接重写
setTimeout等全局方法,适合简单场景; -
modern 模式(推荐):基于
@sinonjs/fake-timers,支持毫秒级精确推进、冻结/快进/单步执行,能准确模拟事件循环行为,尤其适合测试异步链、Promise 时序、微任务交互。
在测试文件顶部或 beforeEach 中启用:
import { vi, it, expect, beforeEach, afterEach } from 'vitest';
beforeEach(() => {
vi.useFakeTimers({ legacyFakeTimers: false }); // 启用 modern 模式
});
afterEach(() => {
vi.useRealTimers(); // 清理,避免影响其他测试
});
用 advanceTimeBy 精确控制时间流逝
不再靠 await vi.runAllTimers() 一股脑执行所有,而是用 vi.advanceTimeBy(ms) 模拟真实时间推移,触发对应毫秒内到期的定时器回调——这正是“高精度”的核心。
例如测试一个每 500ms 轮询、超时 3s 取消的逻辑:
it('should cancel polling after 3 seconds', async () => {
const mockFetch = vi.fn().mockResolvedValue({ data: 'ok' });
let pollCount = 0;
const startPolling = () => {
const timer = setInterval(async () => {
await mockFetch();
pollCount++;
if (pollCount >= 6) clearInterval(timer); // 6 × 500ms = 3s
}, 500);
};
startPolling();
// 推进 2.5 秒:应触发 5 次
vi.advanceTimeBy(2500);
await vi.waitFor(() => expect(pollCount).toBe(5));
// 再推进 500ms:第 6 次触发,然后 clearInterval
vi.advanceTimeBy(500);
await vi.waitFor(() => expect(pollCount).toBe(6));
expect(mockFetch).toHaveBeenCalledTimes(6);
});
结合 waitFor 处理异步回调与 Promise 链
当定时器回调里有 await 或返回 Promise 时,Vitest 的 vi.waitFor 会自动等待 microtask 队列清空,确保 Promise.then、async 函数体执行完毕后再断言。
比如测试一个带防抖的搜索函数:
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
it('debounces correctly with async callback', async () => {
const mockApiCall = vi.fn().mockResolvedValue('result');
const debouncedSearch = debounce(mockApiCall, 300);
debouncedSearch('a');
debouncedSearch('ab');
debouncedSearch('abc');
// 300ms 后才应执行最后一次
vi.advanceTimeBy(300);
await vi.waitFor(() => expect(mockApiCall).toHaveBeenCalledWith('abc'));
expect(mockApiCall).toHaveBeenCalledTimes(1);
});
注意清除与资源隔离
每次测试后调用 vi.useRealTimers() 是必须的,否则 fake timers 会污染后续测试。如果测试中手动保存了 timer ID(如 const id = setTimeout(...)),不需要显式 clearTimeout(id) —— fake timers 在 useRealTimers() 时自动清理所有挂起任务。
若需提前取消某次定时器(比如验证 clearTimeout 是否被正确调用),可保留 ID 并用 vi.cancelTimer(id) 模拟清除行为。


















