首页
/ supabase 仓库中的 Vitest 配置实践:vitest.config.ts 核心选项与多包测试体系详解

supabase 仓库中的 Vitest 配置实践:vitest.config.ts 核心选项与多包测试体系详解

2026-09-05 19:13:49作者:韦蓉瑛

Vitest 的配置文件是整个测试体系的地基:它决定测试在什么环境中运行、如何发现测试文件、失败后如何重试、覆盖率如何统计。本文基于 supabase 仓库中的 Vitest 配置参考文档(core-config.md)展开,并结合仓库内 apps/studioapps/docsapps/wwwpackages/ui 等真实 vitest.config.ts 文件,讲清楚配置加载优先级、核心选项的取值与语义,以及大型 Monorepo 如何组织多包测试配置。读完后你可以独立完成一个新包的 Vitest 配置,并读懂本仓库各应用包的测试脚本行为。

配置加载机制:vitest.config.ts 与 vite.config.ts

Vitest 从 vitest.config.tsvite.config.ts 读取配置,且与 Vite 共享同一套配置格式——测试专属配置全部挂在 test 属性下,其余字段就是标准 Vite 配置(如 pluginsresolve.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/studiopackages/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.tsbeforeAll 里注入本地 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 vitestpnpm 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_modulesdist 等。仓库中的实际用法展示了两个常见技巧:

  1. 展开 configDefaults.exclude 再追加自定义项,避免默认排除项被覆盖。apps/wwwapps/studio 都这样写:
// apps/www
exclude: [...configDefaults.exclude, '.next/*'],
  1. 按文件粒度排除不稳定的大测试。apps/studioexclude 里除了 .next/*(Next.js 产物目录)外,还单独排除了 tests/features/logs/logs-query.test.tsxtests/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.tsreporter: ['text', 'text-summary', 'lcov']include: ['lib/**/*.ts'] 只统计核心逻辑目录,并排除 **/*.test.ts(x) 自身与个别无需覆盖的工具文件;
  • packages/ui/vitest.config.tsreporter: ['lcov']include 覆盖组件库源码 src/**/*.{ts,tsx},配合 package.json 中的 test:civitest --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/studioIS_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.tsapps/docsapps/studioapps/wwwpackages/uipackages/ui-patternspackages/ai-commandspackages/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.aliasplugins 在测试中直接生效,本仓库各配置普遍依赖 vite-tsconfig-paths@vitejs/plugin-react 插件;
  • vitest.config.ts 优先于 vite.config.ts;自定义路径用 --config 指定;
  • process.env.VITEST 在测试运行时为 'true'
  • 测试专属选项全部收敛在 test 属性下,其余字段是标准 Vite 配置;
  • 仓库实战经验:setupFiles(每文件)与 globalSetup(全局一次)分工明确;exclude 记得展开 configDefaults.excluderetry 按 CI/本地分层设置;coverage.include 只圈定真正想统计的源码目录。

掌握以上内容后,你既能按参考文档快速写出新包的 vitest.config.ts,也能对照 apps/studio/vitest.config.tsapps/docs/vitest.config.ts 等真实配置理解每个选项在本仓库测试体系中的实际落点。

登录后查看全文
热门项目推荐
相关项目推荐