supabase 仓库中的 Vitest 配置实践:vitest.config.ts 核心选项与多包测试体系详解
Vitest 的配置文件是整个测试体系的地基:它决定测试在什么环境中运行、如何发现测试文件、失败后如何重试、覆盖率如何统计。本文基于 supabase 仓库中的 Vitest 配置参考文档(core-config.md)展开,并结合仓库内 apps/studio、apps/docs、apps/www、packages/ui 等真实 vitest.config.ts 文件,讲清楚配置加载优先级、核心选项的取值与语义,以及大型 Monorepo 如何组织多包测试配置。读完后你可以独立完成一个新包的 Vitest 配置,并读懂本仓库各应用包的测试脚本行为。
配置加载机制:vitest.config.ts 与 vite.config.ts
Vitest 从 vitest.config.ts 或 vite.config.ts 读取配置,且与 Vite 共享同一套配置格式——测试专属配置全部挂在 test 属性下,其余字段就是标准 Vite 配置(如 plugins、resolve.alias)。这意味着 Vite 的转换管线在测试中同样生效:resolve.alias、插件都能直接复用,这是 Vitest 相对独立测试框架的一个关键设计收益。
配置解析有几条明确的优先级与约定:
vitest.config.ts优先于vite.config.ts;- 可通过
--config命令行参数指定自定义配置路径; - 运行测试时
process.env.VITEST会被设置为true,可用于区分"是否处于测试环境"; - 测试专用选项一律写在
test属性内。
基础配置:vitest.config.ts
最小可用的独立测试配置如下:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// test options
},
})
supabase 仓库中 packages/ai-commands 包就是一个典型的独立配置示例(packages/ai-commands/vitest.config.ts):
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'node',
testTimeout: 30000,
setupFiles: ['./vitest.setup.ts'],
},
})
这个 AI 命令包跑的是 Node 侧逻辑,不需要 DOM 环境,因此 environment 显式设为 'node',并把 testTimeout 从默认的 5000ms 放宽到 30000ms——AI 相关测试通常涉及网络或模型调用,默认超时不够用。
与已有 Vite 配置共存
如果项目已有 vite.config.ts,可以直接在其上扩展 test 属性,同时加上类型引用让 TypeScript 识别该字段:
// vite.config.ts
/// <reference types="vitest/config" />
import { defineConfig } from 'vite'
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
},
})
注意 /// <reference types="vitest/config" /> 这一行的作用:它把 Vitest 的类型扩展注入到 Vite 的 UserConfig 中,使 test 字段获得类型提示与校验,而无需引入额外的类型声明文件。
合并配置:mergeConfig
当 Vite 配置与测试配置分处两个文件、又不想互相 import 副作用时,可以用 mergeConfig 显式合并:
// vitest.config.ts
import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(viteConfig, defineConfig({
test: {
environment: 'jsdom',
},
}))
mergeConfig 是 Vite 提供的深合并工具,会正确合并数组(如 plugins 追加)与对象(如 resolve.alias 按 key 合并),避免手写展开运算符导致的字段覆盖。
核心配置选项逐一拆解
参考文档列出了最常用的 test 选项全集,下面结合仓库中的真实用法逐项说明。
defineConfig({
test: {
// 启用全局 API(describe、it、expect),无需 import
globals: true,
// 测试环境:'node'、'jsdom'、'happy-dom'
environment: 'node',
// 每个测试文件执行前运行的 setup 文件
setupFiles: ['./tests/setup.ts'],
// 测试文件匹配规则
include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'],
// 排除规则
exclude: ['**/node_modules/**', '**/dist/**'],
// 测试超时(ms)
testTimeout: 5000,
// 钩子超时(ms)
hookTimeout: 10000,
// 默认进入 watch 模式
watch: true,
// 覆盖率配置
coverage: {
provider: 'v8', // 或 'istanbul'
reporter: ['text', 'html'],
include: ['src/**/*.ts'],
},
// 隔离执行(每个文件独立进程)
isolate: true,
// 执行池:'threads'、'forks'、'vmThreads'
pool: 'threads',
// 线程/进程数
poolOptions: {
threads: {
maxThreads: 4,
minThreads: 1,
},
},
// 测试间自动清空 mock
clearMocks: true,
// 测试间自动恢复 mock
restoreMocks: true,
// 失败重试次数
retry: 0,
// 首次失败即停止(0 为不启用)
bail: 0,
},
})
globals、environment:环境选择与全局 API
environment: 'node'适合纯逻辑、服务端代码(如 packages/ai-commands/vitest.config.ts);environment: 'jsdom'适合渲染 React 组件的测试。apps/studio与packages/ui均为 UI 应用/组件库,配置里都写了environment: 'jsdom'(见 apps/studio/vitest.config.ts);globals: true免去每个文件import { describe, it, expect } from 'vitest',apps/studio即采用该方式。
值得一提的是,apps/studio 的配置中留下了一条 TODO 注释:
environment: 'jsdom', // TODO(kamil): This should be set per test via header in .tsx files only
这说明团队有意让环境默认值全局生效,未来计划逐步迁移为在单个 .tsx 测试文件头部按需声明环境,实现"全局 node、局部 jsdom"的精细控制。
setupFiles 与 globalSetup:两个容易混淆的钩子
setupFiles 在每个测试文件的工作进程内执行,适合注入 polyfill、mock、环境变量;而 globalSetup 在整个测试运行开始时于主进程执行一次,适合准备共享资源(数据库、静态文件等)。
apps/docs 同时使用了两者,是理解二者分工的好样本:
export default defineConfig({
test: {
// 排除 examples 目录,避免被误当作测试发现路径
exclude: ['examples/**/*', '**/node_modules/**'],
setupFiles: ['vitest.setup.ts'],
globalSetup: ['vitest.globalSetup.ts'],
},
plugins: [
tsconfigPaths({
root: import.meta.dirname,
// 只扫描本应用自己的 tsconfig,避免误扫 examples/** 子目录
projects: ['tsconfig.json'],
}),
],
})
- apps/docs/vitest.setup.ts 在
beforeAll里注入本地 Supabase 连接环境变量(NEXT_PUBLIC_SUPABASE_URL指向http://localhost:54321),并用vi.mock('server-only', ...)屏蔽 Next.js 的 server-only 模块限制,afterAll中还原环境变量并doUnmock; - apps/docs/vitest.globalSetup.ts 则把仓库根目录的
examples/目录整体拷贝到apps/docs/examples/,保证CodeSample.test.ts等测试在npx vitest、pnpm test:local或 IDE 单测等绕过pretest生命周期钩子的场景下也能找到 fixture,否则会报 ENOENT。
对照 apps/docs 的 package.json 脚本(apps/docs/package.json):
"test": "pnpm supabase start && pnpm run test:local && pnpm supabase stop",
"test:local": "vitest --exclude \"**/*.smoke.test.ts\"",
"test:local:unwatch": "vitest --exclude \"**/*.smoke.test.ts\" --run",
可以看出命令行参数与配置文件是叠加关系:--exclude 在配置基础上额外排除 smoke 测试,--run 关闭默认 watch 行为以便 CI 一次性跑完。
include / exclude:测试发现与排除
include 默认匹配 **/*.{test,spec}.*,exclude 默认排除 node_modules、dist 等。仓库中的实际用法展示了两个常见技巧:
- 展开
configDefaults.exclude再追加自定义项,避免默认排除项被覆盖。apps/www与apps/studio都这样写:
// apps/www
exclude: [...configDefaults.exclude, '.next/*'],
- 按文件粒度排除不稳定的大测试。
apps/studio的exclude里除了.next/*(Next.js 产物目录)外,还单独排除了tests/features/logs/logs-query.test.tsx与tests/features/reports/storage-report.test.tsx两个文件(apps/studio/vitest.config.ts)。
注意 apps/docs 的配置注释特别强调:exclude 只影响测试发现,不影响 vite-tsconfig-paths 插件对 tsconfig 的扫描,所以插件侧需要单独用 projects: ['tsconfig.json'] 收敛扫描范围。
testTimeout、retry:超时与 CI 抖动策略
testTimeout(单测试超时)与hookTimeout(before/after 类钩子超时)是毫秒数,按需放大,如前文ai-commands包将testTimeout设为 30000;retry是抑制 flaky 测试的标准手段。apps/studio的配置给出了一个值得参考的 CI 分层策略(apps/studio/vitest.config.ts):
const IS_CI = !!process.env.CI
test: {
// 仅在 CI 中重试不稳定测试;本地失败应立即暴露
retry: IS_CI ? 2 : 0,
}
本地开发时 retry: 0 让失败即时可见,CI 环境允许最多 2 次重试来吸收流水线环境的偶发抖动。
coverage:覆盖率
coverage 下可指定 provider('v8' 或 'istanbul')、reporter 与参与统计的文件范围。仓库各包的差异展示了配置意图:
- apps/studio/vitest.config.ts:
reporter: ['text', 'text-summary', 'lcov'],include: ['lib/**/*.ts']只统计核心逻辑目录,并排除**/*.test.ts(x)自身与个别无需覆盖的工具文件; - packages/ui/vitest.config.ts:
reporter: ['lcov'],include覆盖组件库源码src/**/*.{ts,tsx},配合 package.json 中的test:ci(vitest --run --coverage)与test:report(打开coverage/lcov-report/index.html)形成"CI 收集、本地查看"的完整链路; - packages/ui-patterns/vitest.config.ts 的 reporter 为
['text', 'json', 'html'],说明不同包按消费方式(CI 汇总 vs 人工浏览)选择不同的输出格式。
pool、isolate:执行模型
isolate: true让每个测试文件在独立上下文中执行,避免文件间状态串扰,是大型仓库的安全默认;pool: 'threads'使用 Worker Threads 池执行测试,forks走子进程(隔离性更强但启动成本更高),vmThreads提供 V8 隔离上下文;poolOptions.threads.maxThreads / minThreads控制并发规模,与--no-file-parallelism等 CLI 开关配合可以进一步调节。本仓库各包未显式配置pool,使用默认值即可,从各包测试能稳定运行的现状看,默认参数对这类前端/Node 测试场景是足够的(此结论为从仓库现状推断)。
clearMocks / restoreMocks / bail:Mock 卫生与失败策略
clearMocks: true在每个测试前自动清空 mock 的调用记录与实现,省去手写vi.clearAllMocks();restoreMocks: true更进一步,把被vi.spyOn替换的原函数还原,防止跨测试泄漏;bail: 0表示失败后继续跑完剩余测试(设为n则累计失败 n 次后停止),本地调试想"跑一个错一个"时可临时调成 1。
条件化配置:mode 与 process.env.VITEST
配置函数可以接收 mode 参数做条件化分支,例如测试模式下跳过某些插件:
export default defineConfig(({ mode }) => ({
plugins: mode === 'test' ? [] : [myPlugin()],
test: {
// test options
},
}))
同时 process.env.VITEST === 'true' 是另一条可靠的判断路径,适合在共享代码(而非配置文件)中切换行为。apps/studio 的 IS_CI 条件化(见上文 retry 示例)则是同一思路在 CI 维度的延伸:条件分支不必局限于测试/非测试,任何环境变量都可驱动配置。
Monorepo 场景:projects 与多包配置两种组织方式
参考文档给出的 projects 方案是在一个 Vitest 进程内运行多套配置:
defineConfig({
test: {
projects: [
'packages/*',
{
test: {
name: 'unit',
include: ['tests/unit/**/*.test.ts'],
environment: 'node',
},
},
{
test: {
name: 'integration',
include: ['tests/integration/**/*.test.ts'],
environment: 'jsdom',
},
},
],
},
})
projects 数组成员既可以是目录通配(如 'packages/*',每个子目录用自己的配置),也可以是内联配置对象,适合"同一个测试树按 unit/integration 分泳道"的场景。
而 supabase 仓库本身采用的是另一种 Monorepo 组织方式:pnpm workspace + Turbo,每个应用/包各自维护独立的 vitest.config.ts(apps/docs、apps/studio、apps/www、packages/ui、packages/ui-patterns、packages/ai-commands、packages/dev-tools 等),由根目录 package.json 的脚本按 filter 分发执行:
"test:docs": "turbo run test --filter=docs",
"test:ui": "turbo run test --filter=ui",
"test:ui-patterns": "turbo run test --filter=ui-patterns",
"test:studio": "turbo run test --filter=studio"
从源码结构看,这种"每包独立配置 + Turbo 编排"的方式让各包可以完全自主地选择 environment、setup 与覆盖率范围(前文各包的差异即为例证),代价是需要在每个包内重复少量通用配置;两种方案各有取舍,可依据包之间配置的同质程度选择。
关键要点回顾
- Vitest 复用 Vite 的转换管线:
resolve.alias、plugins在测试中直接生效,本仓库各配置普遍依赖vite-tsconfig-paths与@vitejs/plugin-react插件; vitest.config.ts优先于vite.config.ts;自定义路径用--config指定;process.env.VITEST在测试运行时为'true';- 测试专属选项全部收敛在
test属性下,其余字段是标准 Vite 配置; - 仓库实战经验:
setupFiles(每文件)与globalSetup(全局一次)分工明确;exclude记得展开configDefaults.exclude;retry按 CI/本地分层设置;coverage.include只圈定真正想统计的源码目录。
掌握以上内容后,你既能按参考文档快速写出新包的 vitest.config.ts,也能对照 apps/studio/vitest.config.ts、apps/docs/vitest.config.ts 等真实配置理解每个选项在本仓库测试体系中的实际落点。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00