ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

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

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 兼容 APIadvanced-vi.md是其中专门负责vi.*高级 API 的参考页与features-mocking.md、core-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.calls、mock.results等实现保留mockReset()会连实现一起清掉mockRestore()仅对vi.spyOn创建的 spy 有意义用于恢复被 spy 的原方法。本仓库中的组件测试大量依赖这套语义。例如 CreateAPIKeyDialogs.test.tsx 中mock 函数的默认行为通过beforeEach里的mockUseQueryState.mockReturnValue([, mockSetVisible])逐用例设定并在每个用例前调用vi.clearAllMocks()清空历史——这正是历史隔离 实现按用例重设的标准组合。3. Spyingvi.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. 模块 Mockvi.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最核心的机制是静态提升hoistingVitest 在转换测试文件时会把vi.mock调用提升到所有import之前执行因此工厂函数内部的代码在模块加载前就已生效。这直接带来一个约束——工厂函数内不能直接引用文件顶部的普通变量提升后变量尚未初始化后文第 9 节的vi.hoisted正是为解决这个约束而设计的。部分 mockpartial mock是大型项目中最常用的形态通过importOriginal拿到真实模块并展开只替换个别导出。仓库中 CreateAPIKeyDialogs.test.tsx 是教科书级示例vi.mock(next/navigation, async () { const actual await vi.importActualtypeof import(next/navigation)(next/navigation) return { ...actual, useParams: () ({ ref: project-ref }), } }) vi.mock(nuqs, async () { const actual await vi.importActualtypeof import(nuqs)(nuqs) return { ...actual, useQueryState: mockUseQueryState, } })这里有两个值得注意的细节一是用typeof import(...)给vi.importActual标注了泛型使actual的类型推断与真实模块一致替换时不会丢失类型二是只替换组件真正用到的导出useParams、useQueryState其余导出原样保留把 mock 的爆炸半径控制在最小。route.test.ts 中const actual await vi.importActualtypeof import(~/lib/logger)(~/lib/logger)也是同样的模式。另外两种形态各有用途{ spy: true }spy mode保留原实现的同时记录调用等价于对整个模块做 spyvi.importMock(./module)反向操作在真实模块内部拿到被 mock 的版本适合在被测模块与测试之间共享 mock 引用。5. 动态 Mockvi.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) // Hoistedvi.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()按职责可以分成四组推进advanceTimersByTime(ms)同步推进指定毫秒并触发到期回调带Async的变体会 await 回调含其中的 await适合回调内部还有异步逻辑的场景advanceTimersToNextTimer/advanceTimersToNextFrame则精确到下一个定时器/下一帧后者专为requestAnimationFrame提供全量执行runAllTimers会跑到没有到期定时器为止注意无限递归的定时器会触发溢出保护runOnlyPendingTimers只跑当前已排定的那一批不执行运行中新排定的定时器语义更可控查询状态getTimerCount()返回待执行定时器数量isFakeTimers()判断当前是否处于 fake 模式清理与恢复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/stubEnvNode/浏览器全局对象和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覆盖任意全局符号包括fetch、localStorage等stubEnv修改单个环境变量两者都有配套的批量恢复方法unstubAllGlobals()/unstubAllEnvs()语义上与useFakeTimers/useRealTimers一样强调用完必须还原。仓库实践与此完全对应search/embeddings/route.test.tsvi.stubGlobal(fetch, fetchMock)把整个路由 handler 的外部网络调用收敛到单个 mockoctokit.auth.test.tsvi.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 了三个 mockconst { 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这类重复劳动文档给出了mockObjectconst 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.tsxvi.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 ComponentsafterAll中还原process.env并vi.doUnmock(server-only)。这个 setup 文件本身就是一个viAPI 教学样例环境变量的手动备份还原第 8 节、vi.mockvi.doUnmock的组合第 4、5 节在同 20 行代码里各司其职。15. 关键要点速查汇总文档 Key Points 一节并结合仓库实践补充落地建议vi.mock会被提升到文件顶部——工厂内不能引用未 hoist 的变量需要动态、按用例切换的打桩请用vi.doMock 动态import()并配合vi.resetModules()清缓存参考 getCustomContent.test.tsvi.hoisted是工厂引用外部变量的唯一正路——本仓库组件测试普遍以const { mockA, mockB } vi.hoisted(() ({...}))起手参考 CreateAPIKeyDialogs.test.tsxspy 用于既有方法vi.spyOn保留原实现、支持 getter/setter 维度、可mockRestore参考 SupportAssistant.utils.test.tsFake timers 必须成对设置与恢复useFakeTimerssetSystemTime放beforeEachuseRealTimers放afterEach参考 AccessToken.utils.test.ts异步 UI 断言用vi.waitFor重试到断言通过避免 sleep 硬等待类型层面统一走vi.mocked避免对 mock 值做as any之类的断言每个用例前清零 mock 历史vi.clearAllMocks()或针对性.mockClear()保证调用断言的独立性。整体来看advanced-vi.md覆盖的 API 集合mock 函数、spy、模块/动态 mock、fake timers、时间与全局打桩、hoisted、等待工具、mockObject、配置与全局管理、类型助手在 Supabase 前端仓库的apps/docs、apps/studio、packages/*各测试文件中均有大量对应实例本文给出的每个模式都可以直接到对应路径中对照源码作为编写新测试时的参考基线。【免费下载链接】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),仅供参考
返回列表