假设我们的组件或函数内使用了 setInterval (setTimeout 同理),如果不对其进行任何操作,"傻傻等待"的话,一个单元测试可能就要耗费几秒钟才能完成,甚至还可能超时。
老牌的 jest 或新的 vitest 以及 node:test 原生都支持"时间快进"。但是 bun:test(v1.2.23 2025-10-10)尚未支持 jest/vi.advanceTimersByTime,将报错:
js
// 快进1秒,触发1次 setInterval 回调
37 | vi.advanceTimersByTime(1000)
^
TypeError: vi.advanceTimersByTime is not a function. (In 'vi.advanceTimersByTime(1000)', 'vi.advanceTimersByTime' is undefined)
难道只能"坐以待毙"?不,我们还有一种workaround。就是通过重写或者说 mock 全局 setInterval / setTimeout。
效果一睹为快:

解决办法:重写 setInterval
ts
// src\DeepThinkButton\demo\advanced.test.tsx:
import { afterEach, beforeEach } from 'vitest'
// 1. 保存原始的 setInterval
const originalSetInterval = globalThis.setInterval
beforeEach(() => {
// 在每个测试开始前启用假定时器
// @ts-expect-error
globalThis.setInterval = (callback, delay) => {
// console.log('callback, delay:', { callback, delay })
if (callback.name === 'deepThinkButtonInterval') {
// 返回一个原始 ID,并且内部调用原始函数
return originalSetInterval(() => {
callback()
}, 0)
} else {
return originalSetInterval(callback, delay)
}
}
})
afterEach(() => {
// 在每个测试结束后恢复真定时器
globalThis.setInterval = originalSetInterval
})
使用
假设我们有如下待测试代码,测试深度思考功能:
ts
// 逐个字符显示,当完整内容显示后停止
timer = setInterval(function deepThinkButtonInterval() {
setThinkContent((text) => {
if (text.length < THINK_CONTENT.length) {
// 每次输出 N 个字符
return THINK_CONTENT.slice(0, text.length + 2)
}
clearInterval(timer)
setThinkingStatus('Completed')
setThinkingStopTime(Date.now())
return text
})
}, 20)
执行
ts
❯ bun test src/DeepThinkButton/
正常情况耗时 2s+:
ts
✓ 自定义 icon、按钮尺寸,以及思考内容折叠后效果 [2563.00ms]
第一步修改待测试代码:
diff
- timer = setInterval(() => {
+ timer = setInterval(function deepThinkButtonInterval() {
也就是匿名函数改成具名函数,方便我们针对性 mock。
当我们给单测添加上述 mock 后,耗时减少到 1s+:
ts
✓ 自定义 icon、按钮尺寸,以及思考内容折叠后效果 [1344.00ms]
但其实我们还可以加速,即在一个 interval 回调中多次运行函数:
ts
// src\DeepThinkButton\demo\advanced.test.tsx:
return originalSetInterval(() => {
// 运行 6 次
callback()
callback()
callback()
callback()
callback()
callback()
}, 0)
可以看到又从 1s+ 优化到了 300ms+ ⚡️ !
ts
✓ 自定义 icon、按钮尺寸,以及思考内容折叠后效果 [328.00ms]
成功将耗时从 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 s + 2s+ </math>2s+ 优化到 <math xmlns="http://www.w3.org/1998/Math/MathML"> 300 m s + 300ms+ </math>300ms+,是其 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 / 8 1/8 </math>1/8!
重构
目前只有一个 test case 还好,如果多个 case 需要则要避免违背 DRY 原则,所以接下来我们封装成函数。
ts
// tests/utils.ts
import { afterEach, beforeEach } from 'vitest'
export function advanceInterval(
callbackName: string,
{ ms = 0, batchCalledCount = 1 }: { ms?: number; batchCalledCount?: number } = {},
) {
// 1. 保存原始的 setInterval
const originalSetInterval = globalThis.setInterval
beforeEach(() => {
// 在每个测试开始前启用假定时器
// @ts-expect-error
globalThis.setInterval = (callback, delay) => {
// 返回一个模拟的ID,或者调用原始函数
if (callback.name === callbackName) {
return originalSetInterval(() => {
for (let i = 0; i < batchCalledCount; i++) {
callback()
}
}, ms)
} else {
return originalSetInterval(callback, delay)
}
}
})
afterEach(() => {
// 在每个测试结束后恢复真定时器
globalThis.setInterval = originalSetInterval
})
}
使用:
diff
// src/DeepThinkButton/demo/advanced.test.tsx
import React from 'react'
import { test, expect } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import Demo from './advanced'
import { advanceInterval } from '@/tests/rtl'
+ advanceInterval('deepThinkButtonInterval', { batchCalledCount: 6 })
test('自定义 icon、按钮尺寸,以及思考内容折叠后效果', async () => {
...
})