Supabase 单仓库中的 Vitest 实战:配置、测试 API、Mocking 与覆盖率的完整参考
Supabase 开源仓库(一个 pnpm + Turborepo 管理的多包 monorepo)以 Vitest 作为主要的单元测试框架。本文基于仓库内的技能参考文档 vitest SKILL.md 及其 16 份 reference 展开,覆盖 Vitest 的配置体系、CLI 用法、测试/断言/钩子 API、Mocking、快照、覆盖率、并发与过滤等全部主题,并结合 apps/studio/vitest.config.ts、apps/docs/vitest.config.ts 等仓库真实配置,说明这些 API 在 Supabase 项目中是如何落地的。读完本文,你可以直接在任意 Vite 项目中编写、配置和调试 Vitest 测试。
适用前提:本文的 API 与配置基于 Vitest 3.x(技能文档标注于 2026-01-28 生成,见 GENERATION.md),对应仓库当前 catalog: 依赖锁定的版本;文中所有配置文件示例均来自当前仓库实际内容。
一、为什么是 Vitest
Vitest 是一个由 Vite 驱动的下一代测试框架,提供与 Jest 兼容的 API,开箱即用地支持原生 ESM、TypeScript 和 JSX。它的核心卖点在于与 Vite 共享同一套配置、transformer、resolver 和插件——这意味着你在 vite.config.ts 里配置的别名、路径解析、React 插件可以直接被测试复用,不需要像 Jest 那样再维护一份 jest.config.js 和 moduleNameMapper。
关键特性(引自 SKILL.md):
- Vite 原生:复用 Vite 的转换管线,watch 模式下可像 HMR 一样快速刷新受影响的测试;
- Jest 兼容:大多数 Jest 测试套件可直接迁移;
- 智能 watch 模式:基于模块图只重跑受影响的测试;
- 原生 ESM / TS / JSX:零配置支持;
- 多线程 worker:并行执行测试文件;
- 内置覆盖率:V8 或 Istanbul 两种 provider;
- 快照、Mocking、Spy 工具内置。
在 Supabase 仓库中,Vitest 被广泛使用:根 package.json 通过 Turborepo 分发测试任务,例如 test:studio、test:ui、test:ui-patterns、test:docs;各包内则定义了各自的 vitest.config.ts(共 7 处,包括 apps/docs/vitest.config.ts、apps/studio/vitest.config.ts、apps/www/vitest.config.ts、packages/ui/vitest.config.ts 等)。SKILL.md 的 frontmatter 还特别说明:Studio 特有的组件测试策略应结合 studio-testing 与 studio-mock-api-tests 技能阅读。
二、配置:vitest.config.ts 与 Vite 的融合
参考 core-config.md。
2.1 基本配置方式
Vitest 从 vitest.config.ts 或 vite.config.ts 读取配置,配置格式与 Vite 完全一致:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// 测试选项
},
})
如果已有 Vite 配置,在 vite.config.ts 中加入类型引用和 test 属性即可:
// vite.config.ts
/// <reference types="vitest/config" />
import { defineConfig } from 'vite'
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
},
})
两份配置并存时,可用 mergeConfig 合并:
import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(viteConfig, defineConfig({
test: {
environment: 'jsdom',
},
}))
2.2 常用配置项
defineConfig({
test: {
// 免导入全局 API(describe、it、expect)
globals: true,
// 测试环境: 'node' | 'jsdom' | 'happy-dom'
environment: 'node',
// 每个测试文件执行前运行的 setup 文件
setupFiles: ['./tests/setup.ts'],
// 测试文件匹配
include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'],
// 排除匹配
exclude: ['**/node_modules/**', '**/dist/**'],
// 测试超时(毫秒)
testTimeout: 5000,
// 钩子超时(毫秒)
hookTimeout: 10000,
// 覆盖率配置
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,
// 每个测试之间恢复 spy
restoreMocks: true,
// 失败重试次数
retry: 0,
// 失败 n 次后停止
bail: 0,
},
})
支持条件配置——利用 mode 或 process.env.VITEST 区分测试与运行场景:
export default defineConfig(({ mode }) => ({
plugins: mode === 'test' ? [] : [myPlugin()],
test: {
// test options
},
}))
2.3 仓库实证:apps/studio 的完整配置
apps/studio/vitest.config.ts 是 Supabase 仓库中最典型的一份 Vitest 配置,几乎把上表的核心选项都用上了,并带有一些值得注意的实战技巧:
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
import { configDefaults, defineConfig } from 'vitest/config'
// 部分工具(如 VSCode 的 Vitest 扩展)会把 cwd 设为测试文件所在目录,
// 导致 setupFiles 相对路径解析错误。这里强制基于配置文件位置解析。
const dirname = fileURLToPath(new URL('.', import.meta.url))
const IS_CI = !!process.env.CI
export default defineConfig({
plugins: [
react(),
tsconfigPaths({
projects: ['.'],
}),
],
resolve: {
alias: {
'@ui': resolve(__dirname, './../../packages/ui/src'),
},
},
test: {
globals: true,
environment: 'jsdom',
// 仅在 CI 中重试 flaky 测试;本地失败应立刻暴露
retry: IS_CI ? 2 : 0,
setupFiles: [
resolve(dirname, './tests/setup/polyfills.ts'),
resolve(dirname, './tests/vitestSetup.ts'),
resolve(dirname, './tests/setup/radix.js'),
],
// 不扫描 Next.js 构建产物目录
exclude: [
...configDefaults.exclude,
`.next/*`,
'tests/features/logs/logs-query.test.tsx',
'tests/features/reports/storage-report.test.tsx',
],
reporters: [['default']],
coverage: {
reporter: ['text', 'text-summary', 'lcov'],
exclude: [
'**/*.test.ts',
'**/*.test.tsx',
'**/base64url.ts',
],
include: ['lib/**/*.ts'],
},
},
})
从这份配置可以看到几个 Supabase 的务实选择(见 vitest.config.ts#L14-L54):
environment: 'jsdom':Studio 是 React 组件密集的应用,绝大多数组件测试需要 DOM API;retry: IS_CI ? 2 : 0:用环境变量区分 CI 与本地——CI 里给 flaky 测试两次重试机会,本地则要求失败立即可见,这是控制 flaky 噪声的常用手法;setupFiles使用绝对路径:注释明确解释了原因——Vitest 的编辑器扩展以测试文件目录作为 cwd,相对路径会被解析到错误位置;coverage.include: ['lib/**/*.ts']:覆盖率只统计lib目录,避免 UI 组件文件稀释指标;@ui别名指向 monorepo 内的 packages/ui 源码:得益于 Vite 原生配置共享,Studio 测试直接联编 UI 包源码,无需等待包构建产物。
apps/docs/vitest.config.ts 则展示了另一种轻量用法:setupFiles + globalSetup 双钩子,并用 vite-tsconfig-paths 限制只扫描本应用自己的 tsconfig.json,防止误扫 examples/** 子目录的配置文件:
export default defineConfig({
test: {
exclude: ['examples/**/*', '**/node_modules/**'],
setupFiles: ['vitest.setup.ts'],
globalSetup: ['vitest.globalSetup.ts'],
},
plugins: [
tsconfigPaths({
root: import.meta.dirname,
projects: ['tsconfig.json'],
}),
],
})
2.4 配置要点
- Vitest 复用 Vite 的转换管线,
resolve.alias、plugins 直接生效; vitest.config.ts优先级高于vite.config.ts;- 可用
--config指定自定义配置路径; - 测试运行时
process.env.VITEST会被置为true; - 配置中
test属性之外的一切都是标准 Vite 配置。
三、CLI:命令、选项与 CI 分片
参考 core-cli.md。
3.1 子命令
vitest # 开发环境进入 watch 模式,CI 中单次运行
vitest foobar # 运行路径包含 "foobar" 的测试
vitest basic/foo.test.ts:10 # 按文件和行号运行单个测试
vitest run # 单次运行(无 watch)
vitest run --coverage # 单次运行并输出覆盖率
vitest related src/index.ts src/utils.ts --run # 运行导入了指定文件的测试
vitest bench # 仅运行基准测试
vitest list --json # 列出匹配的测试但不运行
vitest init browser # 初始化项目(浏览器测试场景)
3.2 常用选项
# 配置
--config <path> # 指定配置文件
--project <name> # 只运行指定 project
# 过滤
--testNamePattern, -t # 按测试名模式运行
--changed # 只运行变更文件相关的测试
--changed HEAD~1 # 上一次提交的变更
# 报告器
--reporter <name> # default, verbose, dot, json, html
# 覆盖率
--coverage # 启用覆盖率
--coverage.provider v8 # 指定 v8 provider
# 执行
--shard <index>/<count> # 跨机器分片
--bail <n> # 失败 n 次后停止
--retry <n> # 失败重试
--sequence.shuffle # 随机测试顺序
# watch 模式
--no-watch # 关闭 watch
--standalone # 启动但不立即运行测试
# 环境
--environment <env> # jsdom, happy-dom, node
--globals # 启用全局 API
# 调试
--inspect # 启用 Node inspector
--inspect-brk # 启动即断点
# 输出
--silent # 抑制 console 输出
3.3 Supabase 仓库中的 package.json 脚本
apps/studio/package.json 定义的脚本是最直接的 CLI 用法参考:
{
"scripts": {
"test": "vitest --run --coverage",
"test:watch": "vitest watch",
"test:ui": "vitest --ui",
"test:update": "vitest --run --update",
"test:ci": "vitest --run --coverage"
}
}
apps/docs/package.json 则展示了用 -t 做冒烟测试过滤:"test:smoke": "pnpm run codegen:references && vitest -t \"prod smoke test\""——只运行名字匹配 "prod smoke test" 的测试。
3.4 watch 模式快捷键与分片
watch 模式下的交互快捷键:a 跑全部、f 只跑失败项、u 更新快照、p 按文件名过滤、t 按测试名过滤、q 退出。
CI 多机分片(sharding)与报告合并:
# 机器 1/2/3
vitest run --shard=1/3 --reporter=blob
vitest run --shard=2/3 --reporter=blob
vitest run --shard=3/3 --reporter=blob
# 合并分片结果
vitest --merge-reports --reporter=junit
CLI 要点:watch 在开发环境默认开启,process.env.CI 存在时自动切换为 run 模式;选项名 camelCase(--testTimeout)与 kebab-case(--test-timeout)均可;布尔选项可用 --no- 前缀取反。
四、测试 API:test/it 与修饰符
参考 core-test-api.md。
4.1 基础与异步
import { expect, test, it } from 'vitest'
test('adds numbers', () => {
expect(1 + 1).toBe(2)
})
// it 是 test 的别名
it('works the same', () => {
expect(true).toBe(true)
})
// 异步测试:返回的 Promise 会被自动 await
test('returns promise', () => {
return fetchData().then(result => {
expect(result).toBeDefined()
})
})
4.2 选项、修饰符与动态跳过
// 超时(默认 5000ms)与重试
test('slow test', async () => { /* ... */ }, 10_000)
test('with options', { timeout: 10_000, retry: 2 }, async () => { /* ... */ })
// 条件跳过/条件运行
test.skipIf(process.env.CI)('not in CI', () => {})
test.runIf(process.env.CI)('only in CI', () => {})
// 测试体内动态跳过
test('dynamic skip', ({ skip }) => {
skip(someCondition, 'reason')
})
test.only('only this runs', () => {})
test.todo('implement later')
test.fails('expected to fail', () => {
expect(1).toBe(2) // 断言失败 = 测试通过
})
4.3 参数化测试:test.each 与 test.for
test.each([
[1, 1, 2],
[1, 2, 3],
])('add(%i, %i) = %i', (a, b, expected) => {
expect(a + b).toBe(expected)
})
// 对象形式
test.each([
{ a: 1, b: 1, expected: 2 },
])('add($a, $b) = $expected', ({ a, b, expected }) => {
expect(a + b).toBe(expected)
})
// 模板字面量形式
test.each`
a | b | expected
${1} | ${1} | ${2}
`('add($a, $b) = $expected', ({ a, b, expected }) => {
expect(a + b).toBe(expected)
})
// test.for 优于 .each:不展开数组参数
test.for([
[1, 1, 2],
])('add(%i, %i) = %i', ([a, b, expected], { expect }) => {
expect(a + b).toBe(expected)
})
4.4 测试上下文、Fixture 与标签
test('with context', ({ expect, skip, task }) => {
console.log(task.name)
skip(someCondition)
expect(1).toBe(1)
})
// 自定义 fixture(test.extend)
import { test as base } from 'vitest'
const test = base.extend({
db: async ({}, use) => {
const db = await createDb()
await use(db)
await db.close()
},
})
test('query', async ({ db }) => {
const users = await db.query('SELECT * FROM users')
expect(users).toBeDefined()
})
// 精细重试控制与标签
test('flaky', { retry: { count: 3, delay: 1000, condition: /timeout/i } }, async () => {})
test('database test', { tags: ['db', 'slow'] }, async () => {})
// 运行时:vitest --tags db
关键约束:无 body 的 test 会被标记为 todo;CI 中 test.only 会直接抛错,除非配置 allowOnly: true;并发测试必须使用上下文里的 expect 以保证断言正确归属。
五、Describe API:套件组织
参考 core-describe.md。
describe('Math', () => {
test('adds numbers', () => { expect(1 + 1).toBe(2) })
})
// suite 是 describe 的别名
套件支持嵌套(describe('User', () => { describe('when logged in', ...) })、继承式选项(describe('slow tests', { timeout: 30_000 }, () => {}))、全套件修饰符(describe.skip / .skipIf / .runIf / .only / .todo / .concurrent / .sequential / .shuffle,且可链式组合,如 describe.skip.concurrent(...)),以及 describe.each / describe.for 参数化套件。
套件内的钩子与生命周期(详见下一节)共同构成测试的共享 setup 单元。顶层测试归属于一个隐式的 "file suite";嵌套套件继承父级 timeout、retry 等选项;shuffle 的顺序由配置 sequence.seed 决定,可复现。
六、Expect API:断言体系
参考 core-expect.md。Vitest 基于 Chai 断言库,并提供 Jest 兼容 API。
6.1 基础匹配器
test('assertions', () => {
// 相等性
expect(1 + 1).toBe(2) // 严格相等(===)
expect({ a: 1 }).toEqual({ a: 1 }) // 深度相等
// 真值
expect(true).toBeTruthy()
expect(null).toBeNull()
expect('value').toBeDefined()
// 数字
expect(10).toBeGreaterThan(5)
expect(0.1 + 0.2).toBeCloseTo(0.3, 5)
// 字符串 / 数组 / 对象
expect('hello world').toMatch(/world/)
expect([1, 2, 3]).toContain(2)
expect([{ a: 1 }]).toContainEqual({ a: 1 })
expect({ a: { b: 1 } }).toHaveProperty('a.b', 1)
expect({ a: 1 }).toMatchObject({ a: 1 })
// 类型
expect(new Date()).toBeInstanceOf(Date)
})
6.2 错误、Promise、Spy 断言
// 同步错误需包装成函数
expect(() => throwError()).toThrow('message')
expect(() => throwError()).toThrow(CustomError)
// 异步错误用 rejects
await expect(asyncThrow()).rejects.toThrow('error')
await expect(fetchData()).resolves.toEqual({ data: true })
// mock/spy 断言
expect(fn).toHaveBeenCalledTimes(2)
expect(fn).toHaveBeenCalledWith('arg1', 'arg2')
expect(fn).toHaveBeenNthCalledWith(1, 'arg1')
expect(fn).toHaveReturnedWith(value)
6.3 非对称匹配器、软断言与轮询
expect({ id: 1, name: 'test' }).toEqual({
id: expect.any(Number),
name: expect.any(String),
})
expect({ a: 1, b: 2, c: 3 }).toEqual(expect.objectContaining({ a: 1 }))
expect('hello world').toEqual(expect.stringMatching(/world$/))
// 软断言:失败不中断,汇总报告
expect.soft(1).toBe(2)
expect.soft(2).toBe(3)
// 轮询断言:重试直到通过
await expect.poll(() => fetchStatus(), { interval: 100, timeout: 5000 }).toBe('ready')
// 断言计数
expect.assertions(2) // 恰好 2 次
expect.hasAssertions() // 至少 1 次
6.4 扩展自定义匹配器
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling
return {
pass,
message: () =>
`expected ${received} to be within range ${floor} - ${ceiling}`,
}
},
})
使用原则:toBe 用于原始值,toEqual 用于对象/数组,toStrictEqual 额外检查 undefined 属性和稀疏数组;resolves / rejects / poll 必须 await;并发测试中务必使用上下文的 expect。
七、生命周期钩子
参考 core-hooks.md。
7.1 四大基础钩子与"返回清理函数"模式
import { afterAll, afterEach, beforeAll, beforeEach, test } from 'vitest'
beforeAll(async () => { await setupDatabase() })
afterAll(async () => { await teardownDatabase() })
beforeEach(async () => { await clearTestData() })
afterEach(async () => { await cleanupMocks() })
// 推荐模式:before* 返回清理函数,自动在对应 after* 时机执行
beforeAll(async () => {
const server = await startServer()
return async () => { await server.close() }
})
7.2 Around 钩子:洋葱式包裹测试
aroundEach / aroundAll 可以把测试整体包进事务等上下文(必须调用 runTest()):
aroundEach(async (runTest) => {
await db.beginTransaction()
await runTest() // 必须调用
await db.rollback()
})
多个 around 钩子按洋葱层嵌套执行:outer before → inner before → test → inner after → outer after。
7.3 测试体内钩子
import { onTestFailed, onTestFinished, test } from 'vitest'
test('with cleanup', () => {
const db = connect()
onTestFinished(() => db.close()) // 无论成败都执行
onTestFailed(({ task }) => {
console.log('Failed:', task.result?.errors)
})
})
并发测试请使用上下文提供的钩子:test.concurrent('c', ({ onTestFinished }) => { ... })。
默认执行顺序为栈式:beforeAll(顺序)→ beforeEach(顺序)→ test → afterEach(逆序)→ afterAll(逆序),可通过 sequence.hooks: 'stack' | 'list' | 'parallel' 调整。
八、Mocking:vi 工具与模块 mock
参考 features-mocking.md 与 advanced-vi.md。
8.1 函数 mock 与 spy
const fn = vi.fn()
const add = vi.fn((a, b) => a + b)
fn.mockReturnValue(42)
fn.mockReturnValueOnce(1).mockReturnValueOnce(2)
fn.mockResolvedValue({ data: true })
fn.mockImplementation((x) => x * 2)
const spy = vi.spyOn(cart, 'getTotal')
spy.mockReturnValue(200)
spy.mockRestore()
// 状态重置三件套
fn.mockClear() // 只清调用历史
fn.mockReset() // 清历史 + 实现
fn.mockRestore() // 恢复原始(spy)
vi.clearAllMocks() / vi.resetAllMocks() / vi.restoreAllMocks()
8.2 模块 mock 的四种形态
// 1) 整体 mock(会被提升到文件顶部)
vi.mock('./api', () => ({
fetchUser: vi.fn(() => ({ id: 1, name: 'Mock' })),
}))
// 2) 部分 mock:保留其余实现
vi.mock('./utils', async (importOriginal) => {
const actual = await importOriginal()
return { ...actual, specificFunction: vi.fn() }
})
// 3) spy 模式:保留实现、只追踪调用
vi.mock('./calculator', { spy: true })
// 4) __mocks__ 目录手动 mock(无需 factory)
vi.mock('axios')
vi.mock('./api/client')
动态 mock 不提升,配合动态 import 使用:
test('dynamic mock', async () => {
vi.doMock('./config', () => ({ apiUrl: 'http://test.local' }))
const { apiUrl } = await import('./config')
vi.doUnmock('./config')
})
当 mock 工厂需要引用外部变量时,用 vi.hoisted 解决提升顺序问题:
const mockFn = vi.hoisted(() => vi.fn())
vi.mock('./module', () => ({ getData: mockFn }))
8.3 定时器、日期、全局与环境变量
vi.useFakeTimers()
setTimeout(fn, 1000)
vi.advanceTimersByTime(1000) // 同步
await vi.advanceTimersByTimeAsync(100) // 异步回调
vi.runAllTimers() / vi.runOnlyPendingTimers() / vi.clearAllTimers()
vi.useRealTimers()
vi.setSystemTime(new Date('2024-01-01'))
vi.stubGlobal('fetch', vi.fn(() => Promise.resolve({ json: () => ({ data: 'mock' }) })))
vi.unstubAllGlobals()
vi.stubEnv('API_KEY', 'test-key')
vi.unstubAllEnvs()
可以在配置层开启自动清理(见 features-mocking.md 的 "Config Auto-Reset"):clearMocks / mockReset / restoreMocks / unstubEnvs / unstubGlobals 均为 true,避免 mock 状态在测试间泄漏。
8.4 vi 的其他实用工具
advanced-vi.md 还列出了:vi.isMockFunction、vi.mockObject(obj, { spy: true })(整体 mock 对象)、vi.resetModules() / vi.dynamicImportSettled()(模块缓存管理)、vi.waitFor / vi.waitUntil(重试等待工具)、vi.setConfig({ testTimeout }),以及 TypeScript 下的类型助手 vi.mocked(myFn)(含 { deep: true }、{ partial: true } 选项)。
Supabase 仓库的补充约定:在 Studio 组件测试中,studio-mock-api-tests 技能明确要求不要用
vi.mock('@/data/...')mock 网络请求,而是通过 MSW 在网络层拦截(addAPIMock,见 apps/studio/tests/lib/msw.ts),因为模块级 mock 会绕过 React Query 的缓存/重试/失效路径,并与 OpenAPI 类型漂移脱节。vi.mock只应留给非网络关注点,例如想 stub 掉重型子组件(Monaco 编辑器)。
九、快照测试
// 文件快照:首次运行生成 __snapshots__/<test-file>.snap
expect(result).toMatchSnapshot()
expect(header).toMatchSnapshot('header') // 提示词用于同一测试多个快照
// 内联快照:直接写回测试文件
expect(data).toMatchInlineSnapshot()
// 文件快照:适合 HTML / 大 JSON
await expect(html).toMatchFileSnapshot('./expected/component.html')
// 对象形状匹配(随机字段用非对称匹配器占位)
expect(data).toMatchSnapshot({
id: expect.any(Number),
created: expect.any(Date),
})
// 错误快照
expect(() => { throw new Error('Bad input') })
.toThrowErrorMatchingInlineSnapshot(`[Error: Bad input]`)
更新快照:vitest -u / vitest --update,或在 watch 模式按 u。可通过 expect.addSnapshotSerializer 或配置 snapshotSerializers 添加自定义序列化器;用 snapshotFormat 控制 printBasicPrototype、escapeString 等输出细节;用 resolveSnapshotPath 自定义 .snap 存放位置。并发测试中同样要使用上下文 expect。仓库内可找到真实快照文件,例如 apps/docs/app/middleware.test.tsx.snap 所在应用与 apps/studio 的测试快照,验证了"快照提交进版本控制、在 code review 中审阅快照变更"的约定。
十、代码覆盖率
vitest run --coverage
defineConfig({
test: {
coverage: {
provider: 'v8', // v8 更快(默认);istanbul 兼容性更广
enabled: true,
reporter: ['text', 'json', 'html'],
include: ['src/**/*.{ts,tsx}'],
exclude: ['node_modules/', 'tests/', '**/*.d.ts', '**/*.test.ts'],
all: true, // 报告未覆盖文件
thresholds: {
lines: 80, functions: 75, branches: 70, statements: 80,
perFile: true,
},
},
},
})
- Provider 需单独安装:
@vitest/coverage-v8或@vitest/coverage-istanbul(Studio 在 apps/studio/package.json 中以@vitest/coverage-v8: "catalog:"引入); - Reporter 可选
text/text-summary/json/html/lcov/cobertura,输出目录由reportsDirectory指定(默认./coverage); - 忽略覆盖代码用注释,且需加
-- @preserve才能在 esbuild 处理后存活:
/* v8 ignore next -- @preserve */
function ignored() { return 'not covered' }
/* istanbul ignore if -- @preserve */
if (condition) { /* ignored */ }
分片 CI 下,各分片加 --coverage --reporter=blob,最后 vitest --merge-reports --coverage --reporter=json 合并。
回到 apps/studio/vitest.config.ts#L44-L52 的 coverage 配置:reporter 为 ['text', 'text-summary', 'lcov'](lcov 供 CI 上传使用),include 只统计 lib/**/*.ts,并显式排除了测试文件与第三方测试已覆盖的 base64url.ts——这正是"覆盖率配置服务于 CI 门禁"的实际形态。
十一、并发、隔离与过滤
参考 features-concurrency.md 与 features-filtering.md。
11.1 执行模型
- 文件级并行:测试文件默认跨 worker 并行执行(
fileParallelism: true),池类型可选threads(默认)、forks(隔离更好、更慢)、vmThreads(每文件独立 VM 沙箱); - 文件内并发:
test.concurrent/describe.concurrent让同一文件内的测试并行,断言必须走上下文expect;test.sequential可强制串行;maxConcurrency限制单文件并发数; - 隔离:
isolate: true(默认)为每个文件提供独立环境,isolate: false更快但风险更高; - 顺序控制:
sequence.shuffle打乱顺序、sequence.seed保证可复现,用于暴露测试间隐藏依赖。
11.2 过滤测试
vitest user # 路径过滤,可多模式
vitest src/user.test.ts:25 # 按行号
vitest -t "login" # 按测试名(支持 /user|auth/ 正则)
vitest --changed HEAD~1 # 按 git 变更
vitest related src/utils.ts --run # 导入指定文件的测试(适合 lint-staged)
vitest --tags db --tags slow # 标签过滤
vitest list --filesOnly # 只列出文件,不运行
配置侧还有 tags 白名单 + strictTags: true(未知标签直接失败)、includeSource(源码内测试)等选项。watch 模式支持 p(文件名)/ t(测试名)/ a(全部)/ f(仅失败)交互式过滤。
十二、进阶:环境、类型测试与 Projects
12.1 测试环境(node / jsdom / happy-dom)
参考 advanced-environments.md。默认环境是 node(无浏览器 API);需要 DOM 时装 jsdom(完整模拟)或 happy-dom(更快、API 较少):
defineConfig({
test: {
environment: 'jsdom',
environmentOptions: {
jsdom: { url: 'http://localhost:3000' },
},
},
})
单文件可用文件头魔法注释覆盖:// @vitest-environment jsdom。Studio 选择全局 jsdom 并在 vitest.config.ts#L28 留下 TODO——"未来应只在 .tsx 文件中通过文件头按测试设置",即向 per-file 魔法注释模式演进。外部依赖报 CSS/资源错误时可用 server.deps.inline: ['problematic-package'] 修复;真实浏览器测试则用独立的 Browser Mode(test.browser,配合 Playwright),与"环境"是不同概念。
12.2 类型测试(expectTypeOf / assertType)
参考 advanced-type-testing.md。类型测试放在 .test-d.ts 文件中,配合 typecheck.enabled: true 配置运行(vitest typecheck 或 vitest --typecheck / --typecheck.only):
expectTypeOf<string>().toBeString()
expectTypeOf(greet).parameters.toEqualTypeOf<[string]>()
expectTypeOf(greet).returns.toBeString()
expectTypeOf<User>().toHaveProperty('name').toBeString()
expectTypeOf<B>().toMatchTypeOf<A>() // 子集匹配
expectTypeOf<A>().not.toEqualTypeOf<B>() // 精确相等
// 品牌类型互不相等
expectTypeOf<UserId>().not.toEqualTypeOf<PostId>()
运行时与类型断言可在同一文件混用。assertType<T>(value) 做纯类型断言;// @ts-expect-error 则可验证"某写法必然报类型错误"。
12.3 Projects:一份配置跑多种测试形态
参考 advanced-projects.md。test.projects 允许在同一 Vitest 进程中运行多组不同配置——glob 指向各包的 vitest.config.ts(monorepo 模式),或内联不同 name / include / environment 的对象(例如 unit 用 node、integration 用 jsdom、browser 用 test.browser)。运行指定项目用 vitest --project unit,排除用 --project.ignore browser。项目间可通过 provide 注入配置值、在测试内用 inject('apiUrl') 取出;每个项目可拥有独立的 globalSetup、pool、isolate。从源码结构看,Supabase 目前的 7 份 vitest.config.ts 分散在各 app/package 内、由 Turborepo 分别调度,尚未使用 projects 聚合一处——这属于该特性在 monorepo 场景下可选的演进方向,而非仓库现状。
12.4 Test Context 与 Fixture 进阶
参考 features-context.md。除 task / expect / skip / onTestFinished / onTestFailed 外,test.extend 的 fixture 支持:
- 惰性初始化:只有测试解构到该 fixture 时才执行 setup;
{ auto: true }:每个测试强制运行(适合全局 setup fixture);{ scope: 'file' }/{ scope: 'worker' }:整文件 / 整 worker 只初始化一次的昂贵资源(如数据库连接);- 注入式 fixture:
['/default', { injected: true }]从项目provide取值; test.scoped({ ... }):按套件覆盖 fixture 值;- 组合:可从另一个扩展测试继续
extend,实现 fixture 分层继承;扩展后的test.beforeEach(({ db }) => ...)同样能感知 fixture 类型。
十三、参考文档速查
vitest 技能目录 的 SKILL.md 是本文的骨架,其下 references/ 目录按三层组织,可与本文章节对照使用:
| 层 | 参考文档 | 主题 |
|---|---|---|
| Core | core-config.md | Vite 配置融合、defineConfig |
| Core | core-cli.md | CLI 命令与选项 |
| Core | core-test-api.md | test/it、修饰符、参数化 |
| Core | core-describe.md | 套件、嵌套、修饰符链 |
| Core | core-expect.md | 匹配器、非对称匹配器 |
| Core | core-hooks.md | 生命周期与 around 钩子 |
| Features | features-mocking.md | 函数/模块/定时器 mock |
| Features | features-snapshots.md | 文件/内联/文件快照 |
| Features | features-coverage.md | V8/Istanbul 覆盖率 |
| Features | features-context.md | 上下文与 test.extend 夹具 |
| Features | features-concurrency.md | 并发、分片、pool |
| Features | features-filtering.md | 名称/标签/变更过滤 |
| Advanced | advanced-vi.md | vi 工具全集 |
| Advanced | advanced-environments.md | jsdom/happy-dom/自定义环境 |
| Advanced | advanced-type-testing.md | expectTypeOf/assertType |
| Advanced | advanced-projects.md | 多项目工作区 |
十四、小结
本文以仓库内的 Vitest 技能文档为骨架,完整覆盖了从配置(Vite 融合、mergeConfig、projects)、CLI(分片、过滤、watch 快捷键)、测试 API(修饰符、参数化、上下文)到断言(非对称匹配器、软断言、轮询)、生命周期钩子(around、onTestFinished)、Mocking(提升、hoisted、fake timers)、快照、覆盖率(V8/Istanbul、阈值、ignore 注释)、并发模型与类型测试的全部要点,并以 apps/studio/vitest.config.ts 的 jsdom + CI 条件重试 + 限定覆盖范围的配置,和 apps/docs/vitest.config.ts 的 globalSetup + tsconfig 路径约束作为真实落地佐证。对于要在 Supabase 仓库或任意 Vite 项目中编写测试的开发者,这套参考加上仓库各包现成的 vitest.config.ts,即可覆盖绝大多数单元测试场景;组件级网络测试则应进一步阅读 studio-mock-api-tests 技能,在网络层用 MSW 而非模块 mock。
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 StartedRust0627
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