首页
/ Supabase 单仓库中的 Vitest 实战:配置、测试 API、Mocking 与覆盖率的完整参考

Supabase 单仓库中的 Vitest 实战:配置、测试 API、Mocking 与覆盖率的完整参考

2026-09-06 11:50:54作者:羿妍玫Ivan

Supabase 开源仓库(一个 pnpm + Turborepo 管理的多包 monorepo)以 Vitest 作为主要的单元测试框架。本文基于仓库内的技能参考文档 vitest SKILL.md 及其 16 份 reference 展开,覆盖 Vitest 的配置体系、CLI 用法、测试/断言/钩子 API、Mocking、快照、覆盖率、并发与过滤等全部主题,并结合 apps/studio/vitest.config.tsapps/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.jsmoduleNameMapper

关键特性(引自 SKILL.md):

  • Vite 原生:复用 Vite 的转换管线,watch 模式下可像 HMR 一样快速刷新受影响的测试;
  • Jest 兼容:大多数 Jest 测试套件可直接迁移;
  • 智能 watch 模式:基于模块图只重跑受影响的测试;
  • 原生 ESM / TS / JSX:零配置支持;
  • 多线程 worker:并行执行测试文件;
  • 内置覆盖率:V8 或 Istanbul 两种 provider;
  • 快照、Mocking、Spy 工具内置。

在 Supabase 仓库中,Vitest 被广泛使用:根 package.json 通过 Turborepo 分发测试任务,例如 test:studiotest:uitest:ui-patternstest:docs;各包内则定义了各自的 vitest.config.ts(共 7 处,包括 apps/docs/vitest.config.tsapps/studio/vitest.config.tsapps/www/vitest.config.tspackages/ui/vitest.config.ts 等)。SKILL.md 的 frontmatter 还特别说明:Studio 特有的组件测试策略应结合 studio-testingstudio-mock-api-tests 技能阅读。

二、配置:vitest.config.ts 与 Vite 的融合

参考 core-config.md

2.1 基本配置方式

Vitest 从 vitest.config.tsvite.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,
  },
})

支持条件配置——利用 modeprocess.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";嵌套套件继承父级 timeoutretry 等选项;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.mdadvanced-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.isMockFunctionvi.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 编辑器)。

九、快照测试

参考 features-snapshots.md

// 文件快照:首次运行生成 __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 控制 printBasicPrototypeescapeString 等输出细节;用 resolveSnapshotPath 自定义 .snap 存放位置。并发测试中同样要使用上下文 expect。仓库内可找到真实快照文件,例如 apps/docs/app/middleware.test.tsx.snap 所在应用与 apps/studio 的测试快照,验证了"快照提交进版本控制、在 code review 中审阅快照变更"的约定。

十、代码覆盖率

参考 features-coverage.md

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-L52coverage 配置:reporter 为 ['text', 'text-summary', 'lcov'](lcov 供 CI 上传使用),include 只统计 lib/**/*.ts,并显式排除了测试文件与第三方测试已覆盖的 base64url.ts——这正是"覆盖率配置服务于 CI 门禁"的实际形态。

十一、并发、隔离与过滤

参考 features-concurrency.mdfeatures-filtering.md

11.1 执行模型

  • 文件级并行:测试文件默认跨 worker 并行执行(fileParallelism: true),池类型可选 threads(默认)、forks(隔离更好、更慢)、vmThreads(每文件独立 VM 沙箱);
  • 文件内并发test.concurrent / describe.concurrent 让同一文件内的测试并行,断言必须走上下文 expecttest.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 typecheckvitest --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.mdtest.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') 取出;每个项目可拥有独立的 globalSetuppoolisolate。从源码结构看,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。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388