news 2026/9/7 23:48:02

Vitest vi API 深度实战:从 Mock 函数、Fake Timers 到模块打桩的完整手册(Supabase 仓库实践版)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vitest vi API 深度实战:从 Mock 函数、Fake Timers 到模块打桩的完整手册(Supabase 仓库实践版)

Vitest vi API 深度实战:从 Mock 函数、Fake Timers 到模块打桩的完整手册(Supabase 仓库实践版)

【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase

本文以当前仓库的 Vitest 技能参考文档.agents/skills/vitest/references/advanced-vi.md为主体,系统讲解 Vitest 3.x 中vi对象提供的全套 Mocking 与测试工具能力:mock 函数、spy、模块 mock 与动态 mock、fake timers、全局/环境变量打桩、等待类工具与类型辅助。每一节都会结合本仓库(Supabase 前端 monorepo)中的真实测试文件,展示这些 API 在大型 React 项目中的实际落地写法,帮助你在编写组件测试、工具函数测试和 API 路由测试时直接复用成熟模式。

1.vi的定位:Mocking 与测试工具的总入口

参考文档(advanced-vi.md)开篇即给出核心结论:vihelper 提供 mocking 与工具函数,导入方式统一为:

import { vi } from 'vitest'

该文档属于.agents/skills/vitest/技能包的一部分。从技能包的 SKILL.md 元数据看,这套参考基于 Vitest 3.x 生成,覆盖 Vitest 的 Jest 兼容 API;advanced-vi.md是其中专门负责vi.*高级 API 的参考页,与features-mocking.mdcore-config.md等兄弟文档共同构成完整的 Vitest API 索引。理解这一点很重要:vi并不只是"造 mock 的工厂",它同时承担了模块系统控制(mock/unmock/reset)、时间系统控制(fake timers)、全局环境控制(stubGlobal/stubEnv)和测试配置控制(setConfig/resetConfig)四大职责。

2. Mock 函数:vi.fn()的完整能力面

文档给出了 mock 函数的最小完整集合:

// Create mock const fn = vi.fn() const fnWithImpl = vi.fn((x) => x * 2) // Check if mock vi.isMockFunction(fn) // true // Mock methods fn.mockReturnValue(42) fn.mockReturnValueOnce(1) fn.mockResolvedValue(data) fn.mockRejectedValue(error) fn.mockImplementation(() => 'result') fn.mockImplementationOnce(() => 'once') // Clear/reset fn.mockClear() // Clear call history fn.mockReset() // Clear history + implementation fn.mockRestore() // Restore original (for spies)

各方法的关键差异在于作用范围:

  • mockReturnValue/mockResolvedValue/mockRejectedValue:设置恒定返回值/异步结果;
  • mockReturnValueOnce/mockImplementationOnce:仅作用于下一次调用,适合模拟"第一次请求失败、重试后成功"这类序列行为;
  • mockClear()只清空调用历史(mock.callsmock.results等),实现保留;mockReset()会连实现一起清掉;mockRestore()仅对vi.spyOn创建的 spy 有意义,用于恢复被 spy 的原方法。

本仓库中的组件测试大量依赖这套语义。例如 CreateAPIKeyDialogs.test.tsx 中,mock 函数的默认行为通过beforeEach里的mockUseQueryState.mockReturnValue(['', mockSetVisible])逐用例设定,并在每个用例前调用vi.clearAllMocks()清空历史——这正是"历史隔离 + 实现按用例重设"的标准组合。

3. Spying:vi.spyOn监视与替换现有方法

文档中的 spying 示例覆盖了三种典型用法:

const obj = { method: () => 'original' } const spy = vi.spyOn(obj, 'method') obj.method() expect(spy).toHaveBeenCalled() // Mock implementation spy.mockReturnValue('mocked') // Spy on getter/setter vi.spyOn(obj, 'prop', 'get').mockReturnValue('value')

要点:

  • vi.spyOn(obj, 'method')默认保留原实现,只记录调用;随后可以通过任何mockXxx方法替换实现;
  • 第三个参数'get'/'set'允许对访问器属性打 spy,这在测试依赖 getter 的计算属性时很关键;
  • spy 与手动 mock 的最大区别:spy 挂在原对象上,测试结束后可以mockRestore()恢复,避免污染模块状态。

仓库中的真实用例印证了这一点:SupportAssistant.utils.test.ts 中对 Web Storage 的原型方法打 spy——vi.spyOn(Storage.prototype, 'removeItem').mockImplementation(...),用于隔离浏览器存储副作用。这种"spy 原型方法 + 替换实现"是测试 DOM/存储依赖时的惯用手法。

4. 模块 Mock:vi.mock的静态提升与部分 mock

模块 mock 是vi中最需要理解机制的一块。文档给出的完整示例:

// Hoisted to top of file vi.mock('./module', () => ({ fn: vi.fn(), })) // Partial mock vi.mock('./module', async (importOriginal) => ({ ...(await importOriginal()), specificFn: vi.fn(), })) // Spy mode - keep implementation vi.mock('./module', { spy: true }) // Import actual module inside mock const actual = await vi.importActual('./module') // Import as mock const mocked = await vi.importMock('./module')

从源码结构看,vi.mock最核心的机制是静态提升(hoisting):Vitest 在转换测试文件时,会把vi.mock调用提升到所有import之前执行,因此工厂函数内部的代码在模块加载前就已生效。这直接带来一个约束——工厂函数内不能直接引用文件顶部的普通变量(提升后变量尚未初始化),后文第 9 节的vi.hoisted正是为解决这个约束而设计的。

部分 mock(partial mock)是大型项目中最常用的形态:通过importOriginal拿到真实模块并展开,只替换个别导出。仓库中 CreateAPIKeyDialogs.test.tsx 是教科书级示例:

vi.mock('next/navigation', async () => { const actual = await vi.importActual<typeof import('next/navigation')>('next/navigation') return { ...actual, useParams: () => ({ ref: 'project-ref' }), } }) vi.mock('nuqs', async () => { const actual = await vi.importActual<typeof import('nuqs')>('nuqs') return { ...actual, useQueryState: mockUseQueryState, } })

这里有两个值得注意的细节:一是用typeof import('...')vi.importActual标注了泛型,使actual的类型推断与真实模块一致,替换时不会丢失类型;二是只替换组件真正用到的导出(useParamsuseQueryState),其余导出原样保留,把 mock 的爆炸半径控制在最小。route.test.ts 中const actual = await vi.importActual<typeof import('~/lib/logger')>('~/lib/logger')也是同样的模式。

另外两种形态各有用途:

  • { spy: true }(spy mode):保留原实现的同时记录调用,等价于对整个模块做 spy;
  • vi.importMock('./module'):反向操作,在真实模块内部拿到"被 mock 的版本",适合在被测模块与测试之间共享 mock 引用。

5. 动态 Mock:vi.doMock与模块缓存重置

静态vi.mock被提升后无法在运行时按需开关,文档为此给出了动态版本:

// Not hoisted - use with dynamic imports vi.doMock('./config', () => ({ key: 'value' })) const config = await import('./config') // Unmock vi.doUnmock('./config') vi.unmock('./module') // Hoisted

vi.doMock/vi.doUnmock不会被提升,按书写顺序在运行时生效,因此只能配合动态import()使用——先打桩,再动态导入,拿到的才是 mock 后的模块。配套地,vi.resetModules()清空模块缓存(注意它只清"已导入模块"的缓存,与 mock 注册无关),await vi.dynamicImportSettled()用于等待所有已触发的动态导入完成,防止微任务边界上的竞态。

仓库中的 getCustomContent.test.ts 正是这一套组合的完整落地:

vi.resetModules() vi.doMock('./custom-content.json', () => ({ /* 用例 A 的桩数据 */ })) // ... await import 被测函数并断言 vi.doMock('./custom-content.json', () => ({ /* 用例 B 的桩数据 */ }))

这种"同一文件内多个用例分别打不同 JSON 桩"的写法,只有 doMock + resetModules 组合才能做到,是vi.mock无法替代的场景。

6. Fake Timers:可控的时间系统

vi对时间系统的控制是文档中篇幅最大的一节,完整 API 如下:

vi.useFakeTimers() setTimeout(() => console.log('done'), 1000) // Advance time vi.advanceTimersByTime(1000) vi.advanceTimersByTimeAsync(1000) // For async callbacks vi.advanceTimersToNextTimer() vi.advanceTimersToNextFrame() // requestAnimationFrame // Run all timers vi.runAllTimers() vi.runAllTimersAsync() vi.runOnlyPendingTimers() // Clear timers vi.clearAllTimers() // Check state vi.getTimerCount() vi.isFakeTimers() // Restore vi.useRealTimers()

按职责可以分成四组:

  1. 推进advanceTimersByTime(ms)同步推进指定毫秒并触发到期回调;带Async的变体会 await 回调(含其中的 await),适合回调内部还有异步逻辑的场景;advanceTimersToNextTimer/advanceTimersToNextFrame则精确到"下一个定时器/下一帧",后者专为requestAnimationFrame提供;
  2. 全量执行runAllTimers会跑到没有到期定时器为止,注意无限递归的定时器会触发溢出保护;runOnlyPendingTimers只跑当前已排定的那一批,不执行运行中新排定的定时器,语义更可控;
  3. 查询状态getTimerCount()返回待执行定时器数量,isFakeTimers()判断当前是否处于 fake 模式;
  4. 清理与恢复clearAllTimers()清空队列,useRealTimers()恢复真实时间——文档 Key Points 特别强调 fake timers "require explicit setup and teardown",即每个用useFakeTimers()的测试都应成对地useRealTimers()收尾。

仓库中 AccessToken.utils.test.ts 展示了把"成对设置/恢复"固化为 hook 的标准写法:

describe('getExpirationDate', () => { const FIXED_DATE = new Date('2025-06-15T12:00:00.000Z') beforeEach(() => { vi.useFakeTimers() vi.setSystemTime(FIXED_DATE) }) afterEach(() => { vi.useRealTimers() }) // 用例断言 getExpirationDate('hour') === dayjs(FIXED_DATE).add(1, 'hours') })

被测函数getExpirationDate依赖"当前时间"计算 token 过期时间,若不固定系统时间,断言会随运行时刻漂移。这里 fake timers 与下节的setSystemTime组合使用,正是文档所描述能力的直接应用。

7. Mock 系统时间:setSystemTime与时间查询

在 fake timers 之上,vi还提供系统时钟的直接操控:

vi.setSystemTime(new Date('2024-01-01')) expect(new Date().getFullYear()).toBe(2024) vi.getMockedSystemTime() // Get mocked date vi.getRealSystemTime() // Get real time (ms)

setSystemTime修改的是"系统时钟的当前值",advanceTimersByTime推进后该值同步前移,二者配合即可实现完全确定性的时间测试;getMockedSystemTime/getRealSystemTime则分别用于读取被 mock 的时间与真实时间,方便在断言中做差值计算。

仓库中 revalidate/route.test.ts 使用vi.setSystemTime(mockDate)固定时间后测试 revalidate 缓存逻辑;LogTimeRange.utils.test.ts 同样以vi.useFakeTimers()+vi.setSystemTime(new Date('2025-01-08T12:00:00.000Z'))固定"日志时间范围"的边界计算。这两处都验证了文档 API 在实际业务代码中的对应关系:凡是"时间相关的纯函数",一律走这套固定时钟模式。

8. 全局与环境变量打桩:stubGlobal/stubEnv

Node/浏览器全局对象和process.env是测试隔离的高频痛点,文档给出的对应工具:

// Stub global vi.stubGlobal('fetch', vi.fn()) vi.unstubAllGlobals() // Stub environment vi.stubEnv('API_KEY', 'test') vi.stubEnv('NODE_ENV', 'test') vi.unstubAllEnvs()

stubGlobal覆盖任意全局符号(包括fetchlocalStorage等),stubEnv修改单个环境变量,两者都有配套的批量恢复方法unstubAllGlobals()/unstubAllEnvs(),语义上与useFakeTimers/useRealTimers一样强调"用完必须还原"。

仓库实践与此完全对应:

  • search/embeddings/route.test.ts:vi.stubGlobal('fetch', fetchMock),把整个路由 handler 的外部网络调用收敛到单个 mock;
  • octokit.auth.test.ts:vi.stubEnv(name, env[name] ?? '')在循环中对一组 token 环境变量逐个打桩,模拟 GitHub OAuth 配置;
  • 而 vitest.setup.ts 展示了另一种思路:直接在beforeAll里备份oldEnv = { ...process.env }并整体替换,afterAll中还原——对于需要改一整批环境变量的场景,手动备份/恢复比逐条stubEnv更直观,两种方式可按粒度选择。

9.vi.hoisted:在 mock 工厂中引用变量的官方解法

第 4 节提到vi.mock工厂因提升无法引用顶部变量。文档给出的方案是把变量声明也放进提升范围:

const mock = vi.hoisted(() => vi.fn()) vi.mock('./module', () => ({ fn: mock, // Can reference hoisted variable }))

vi.hoisted(fn)会执行传入函数并把返回值提升到文件顶部(同样在import之前求值),因此返回出来的mock既能在工厂内引用,也能在测试主体中引用——这是"mock 工厂与用例共享同一个 mock 引用"的关键桥梁。

这个模式在仓库中被高频使用。CreateAPIKeyDialogs.test.tsx 甚至一次 hoist 了三个 mock:

const { mockSetVisible, mockShortcut, mockUseQueryState } = vi.hoisted(() => ({ mockSetVisible: vi.fn(), mockShortcut: vi.fn(({ children }: any) => <div>// Wait for callback to succeed await vi.waitFor(async () => { const el = document.querySelector('.loaded') expect(el).toBeTruthy() }, { timeout: 5000, interval: 100 }) // Wait for truthy value const element = await vi.waitUntil( () => document.querySelector('.loaded'), { timeout: 5000 } )

区别在于回调语义:waitFor里直接写断言,重试到断言通过为止;waitUntil则是轮询一个函数,重试到返回值 truthy为止,并把该值 resolve 出来供后续使用。两者都支持timeout/interval选项,避免测试因异步竞态而偶发失败(flaky)。这是相对await act(...)/findBy*更通用的底层能力,尤其适合"多个异步源汇聚后才更新状态"的复杂场景。

11.vi.mockObject:批量 mock 对象的所有方法

对于"把整个对象的方法都替换成 mock"这类重复劳动,文档给出了mockObject

const original = { method: () => 'real', nested: { fn: () => 'nested' }, } const mocked = vi.mockObject(original) mocked.method() // undefined (mocked) mocked.method.mockReturnValue('mocked') // Spy mode const spied = vi.mockObject(original, { spy: true }) spied.method() // 'real' expect(spied.method).toHaveBeenCalled()

默认模式会递归地把对象(含nested这类嵌套对象)的所有函数替换为 mock 函数,调用默认返回undefined{ spy: true }则保留实现、只做记录。相比手动逐个vi.fn(),它能保证"对象形状不变 + 方法全部可控",适合 mock SDK 客户端、事件总线这类方法密集的对象。

12. 测试配置与全局 Mock 管理

文档最后两块 API 用于运行时调整测试行为:

vi.setConfig({ testTimeout: 10_000, hookTimeout: 10_000, }) vi.resetConfig()

vi.setConfig允许在测试文件内(如describe块作用域)局部覆盖配置项,vi.resetConfig()还原。典型场景是"某个慢网络模拟用例单独放宽超时",而不必改全局vitest.config

全局 mock 管理三件套:

vi.clearAllMocks() // Clear all mock call history vi.resetAllMocks() // Reset + clear implementation vi.restoreAllMocks() // Restore originals (spies)

三者对应第 2 节单实例方法的批量版:clear清历史、reset连实现一起清、restore恢复 spy 原函数。仓库中 CreateAPIKeyDialogs.test.tsx 的beforeEach(() => { vi.clearAllMocks(); ... })与 StorageExplorer.utils.test.ts 的beforeEach(() => vi.mocked(toast.error).mockClear())都遵循"每用例前清零历史"的原则,保证toHaveBeenCalled类断言只反映当前用例的行为。

13.vi.mocked:被 mock 值的类型助手

TS 项目中常见的痛点是:vi.mock('./module')之后,导入的函数在类型系统里还是"真函数",mockReturnValue等调用会被类型检查拒绝。文档的解法:

import { myFn } from './module' vi.mock('./module') // Type as mock vi.mocked(myFn).mockReturnValue('typed') // Deep mocking vi.mocked(myModule, { deep: true }) // Partial mock typing vi.mocked(fn, { partial: true }).mockResolvedValue({ ok: true })

vi.mocked是纯类型层的转换:把值断言为Mock类型,让mockReturnValue/mockResolvedValue/mock.calls等 API 可被类型安全地调用,同时保留原函数签名的入参/返回值类型信息。{ deep: true }对模块内嵌套属性递归转换,{ partial: true }则放宽对"部分导出也被 mock"的约束。

仓库内该 API 的使用密度非常高。例如 ObservabilityMenu.utils.test.tsx:

vi.mocked(useFlag).mockReturnValue(false) vi.mocked(useParams).mockReturnValue({ ref: REF }) vi.mocked(useSupamonitorStatus).mockReturnValue({ /* 状态对象 */ }) vi.mocked(useIsFeatureEnabled).mockReturnValue(true)

以及 SupportFormPage.test.tsx 中对 toast 的vi.mocked(toast.error).mockImplementation(toastErrorSpy)——先mockImplementation换成 spy,再在断言里验证 toast 的入参。可以推断,vi.mocked配合工厂 mock,已经构成该仓库 TS 测试中"给 hook 返回值按用例定制"的主力写法。

14. 组合运用:仓库测试基础设施如何串联这些 API

把上面的 API 放到仓库的真实基础设施里看,能更完整地理解它们的分工。以apps/docs应用为例:

  • 配置文件 vitest.config.ts 通过setupFiles: ['vitest.setup.ts']globalSetup: ['vitest.globalSetup.ts']注入全局环境。其中 vitest.globalSetup.ts 在测试进程启动前把仓库根目录的examples/复制进应用目录,保证CodeSample.test.ts的 fixture 可读——这是"测试前置条件"的进程级处理;
  • vitest.setup.ts 则处理文件级前置:在beforeAll中注入本地 Supabase 的NEXT_PUBLIC_SUPABASE_URL/NEXT_PUBLIC_SUPABASE_ANON_KEY环境变量,并用vi.mock('server-only', () => ({}))屏蔽 Next.js 的 server-only 守卫(注释明确说明是为了"Prevent errors about importing server-only modules from Client Components"),afterAll中还原process.envvi.doUnmock('server-only')

这个 setup 文件本身就是一个viAPI 教学样例:环境变量的手动备份还原(第 8 节)、vi.mock+vi.doUnmock的组合(第 4、5 节)在同 20 行代码里各司其职。

15. 关键要点速查

汇总文档 Key Points 一节,并结合仓库实践补充落地建议:

  1. vi.mock会被提升到文件顶部——工厂内不能引用未 hoist 的变量;需要动态、按用例切换的打桩请用vi.doMock+ 动态import(),并配合vi.resetModules()清缓存(参考 getCustomContent.test.ts);
  2. vi.hoisted是工厂引用外部变量的唯一正路——本仓库组件测试普遍以const { mockA, mockB } = vi.hoisted(() => ({...}))起手(参考 CreateAPIKeyDialogs.test.tsx);
  3. spy 用于既有方法vi.spyOn保留原实现、支持 getter/setter 维度、可mockRestore(参考 SupportAssistant.utils.test.ts);
  4. Fake timers 必须成对设置与恢复useFakeTimers+setSystemTimebeforeEachuseRealTimersafterEach(参考 AccessToken.utils.test.ts);
  5. 异步 UI 断言用vi.waitFor重试到断言通过,避免 sleep 硬等待;
  6. 类型层面统一走vi.mocked,避免对 mock 值做as any之类的断言;
  7. 每个用例前清零 mock 历史vi.clearAllMocks()或针对性.mockClear()),保证调用断言的独立性。

整体来看,advanced-vi.md覆盖的 API 集合(mock 函数、spy、模块/动态 mock、fake timers、时间与全局打桩、hoisted、等待工具、mockObject、配置与全局管理、类型助手)在 Supabase 前端仓库的apps/docsapps/studiopackages/*各测试文件中均有大量对应实例,本文给出的每个模式都可以直接到对应路径中对照源码,作为编写新测试时的参考基线。

【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 23:47:34

机器学习实战:从数据清洗到模型评估的完整案例精解

简介&#xff1a;《机器学习实战案例精解》是一本面向数据科学家与技术初学者的机器学习实战电子书&#xff0c;以五个真实案例为主线&#xff0c;系统讲解统计与概率、回归、时间序列、聚类与分类等核心方法&#xff0c;适合希望通过完整案例掌握机器学习流程的读者。书中每个…

作者头像 李华
网站建设 2026/9/7 23:46:50

2026语音芯片口碑全解析:选型判断维度、常见方案盘点与适用场景详解,附核心避坑FAQ

一、2026年语音芯片认知现状与核心需求当前处于语音芯片选型决策的熟悉阶段&#xff0c;多数从业者已完成“有需求”的初步认知&#xff0c;核心困惑集中在「选什么类型的方案」「怎么判断产品口碑」「适配自身场景的核心要求是什么」。据《2026中国语音芯片行业应用白皮书》数…

作者头像 李华
网站建设 2026/9/7 23:46:46

Android进程与线程:从源码到保活实践的全链路排查手册

上个月有个做社交产品的朋友找我&#xff0c;说他们在 oppo 手机上后台被杀得特别惨&#xff0c;推送收不到&#xff0c;音乐播放器也经常断。他想了一堆“黑科技”想去保活&#xff0c;问我行不行。我跟他讲&#xff0c;你先别急着上那些骚操作&#xff0c;你先把 Android 的进…

作者头像 李华
网站建设 2026/9/7 23:46:29

BST最近K值:从全量遍历到双栈导航的算法优化

最近在复盘一道老题&#xff1a;Closest K Values in BST。我必须说&#xff0c;这道题给我留下的印象比很多 hard 题都深。原因不是它难&#xff0c;而是它让我重新审视了一个很常见的思维惯性——刷算法题刷久了&#xff0c;人很容易把“解决问题”等同于“遍历所有可能”。B…

作者头像 李华
网站建设 2026/9/7 23:44:56

园区数字化为何难落地?从GB/T46883-2025看数据标准与服务化转型

1. 从烟囱式建设到国标落地&#xff1a;我为什么盯上了这份编号并八三的园区数字化标准前阵子去某产业园区做交流&#xff0c;运营负责人翻着一摞供应商方案跟我诉苦&#xff1a;摄像头厂家说自己的平台全开放&#xff0c;楼宇自控那边却坚持只留OPC UA接口&#xff0c;能耗采集…

作者头像 李华
网站建设 2026/9/7 23:43:03

Linux路径与常用命令实战:从入门到排障

1. Linux文件系统与路径概念入门1.1 为什么路径是Linux操作的第一道门槛很多刚接触Linux的朋友&#xff0c;最容易栽跟头的不是命令记不住&#xff0c;而是搞不清"我现在在哪""我要去的文件在哪""怎么描述这个位置"。Windows时代大家习惯双击图标…

作者头像 李华