Bun Test 不支持时间快进?我用这招让单元测试提速 8 倍!

假设我们的组件或函数内使用了 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 () => {
  ...
})
相关推荐
奶糖的次元空间3 小时前
带你用 Javascript 生成器玩转「会暂停」的函数
node.js
阿蔹5 小时前
Session与Cookies
selenium·测试
Hilaku10 小时前
我是如何用一行 JS 代码,让你的浏览器内存瞬间崩溃的?
前端·javascript·node.js
五仁火烧10 小时前
npm run build命令详解
前端·vue.js·npm·node.js
前端付豪10 小时前
NodeJs 做了什么 Fundamentals Internals
前端·开源·node.js
brave and determined11 小时前
工程设计类学习(DAY5):揭秘HALT试验:产品极限测试全解析
测试·hass·产品设计·halt·高低温循环·产品寿命实验·产品质量
局外人LZ13 小时前
libsodium.js:web端与 Node.js 的现代加密工具集,构建前端安全加密体系
前端·javascript·node.js
寂夜了无痕13 小时前
pnpm:快速、节省空间的 Node.js 包管理器
npm·node.js·pnpm
程序员爱钓鱼14 小时前
Node.js 博客系统实战(一):项目需求分析
前端·后端·node.js
Jing_Rainbow1 天前
【Vue-2/Lesson62(2025-12-10)】模块化与 Node.js HTTP 服务器开发详解🧩
前端·vue.js·node.js