首页
/ Supabase 仓库中的 Vitest 并发与并行测试实践:从文件级并行到 CI 分片

Supabase 仓库中的 Vitest 并发与并行测试实践:从文件级并行到 CI 分片

2026-09-04 13:45:24作者:霍妲思

本篇基于 Supabase 开源仓库中随仓库维护的 Vitest 技能参考文档 features-concurrency.md 展开,系统讲解 Vitest 的并发(concurrency)与并行(parallelism)测试体系:文件如何在多个 worker 间并行调度、单个测试文件内部的测试如何并发执行、如何在 CI 中用分片(sharding)把测试摊到多台机器上。读完本文,你可以直接在类似本仓库的 pnpm monorepo 中落地并发配置,并理解 apps/studiopackages/ai-commands 等模块中的真实用法。

1. 两个层级的"并行":先分清概念

Vitest 的并行执行其实分两个互不相同的层级,混淆它们是最常见的配置误区:

  1. 文件级并行(File Parallelism):多个测试文件被分配到多个 worker 进程/线程中同时执行,由 fileParallelismmaxWorkers/minWorkerspool 等配置控制;
  2. 测试级并发(Concurrent Tests):单个文件内部的多个 test 并发跑(而不是顺序跑),由 test.concurrent / describe.concurrent 等 API 控制。

文件级并行是默认开启的,几乎所有仓库(包括 Supabase 的 studio、docs、www 等 app)不写任何配置就自动受益;测试级并发则需要显式声明,因为 Vitest 默认在同一文件内顺序执行测试。

版本前提:本仓库通过 pnpm-workspace.yaml 的 catalog 机制统一锁定 vitest: ^4.1.4(同版本还有 @vitest/coverage-v8@vitest/ui),各子包(如 apps/studio/package.jsonapps/www/package.json)均以 "vitest": "catalog:" 引用该版本。本文引用的技能文档基于 Vitest 3.x API 生成(见 SKILL.md 的 metadata 说明),上述并发相关 API(.concurrent--shardsequence)在 3.x/4.x 间保持稳定,但具体默认值请以你安装的版本为准。

2. 文件级并行与 worker 配置

2.1 核心配置项

默认情况下 Vitest 就把测试文件并行分发给 worker,显式写法如下:

defineConfig({
  test: {
    // Run files in parallel (default: true)
    fileParallelism: true,

    // Number of worker threads
    maxWorkers: 4,
    minWorkers: 1,

    // Pool type: 'threads', 'forks', 'vmThreads'
    pool: 'threads',
  },
})

参数说明:

  • fileParallelism:布尔值,默认 true。关掉后所有文件在一个 worker 内顺序执行,一般只在排查与并行强相关的诡异 bug 时才需要;
  • maxWorkers / minWorkers:worker 数量的上限与下限。worker 过多会放大内存占用(每个 worker 都是独立的模块实例),在 CI 容器这种资源受限环境里通常要显式压低;
  • pool:worker 的承载技术,可选 'threads'(worker_threads,默认)、'forks'(子进程)、'vmThreads'(VM 隔离线程),第 8 节会逐一分析。

2.2 本仓库的真实配置对照

Supabase 各子包并没有在配置里显式写 fileParallelism——这正是"默认即并行"的体现。看三个有代表性的真实配置:

apps/studio/vitest.config.ts(最大的前端应用,jsdom 环境 + CI 下自动重试):

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',
    // Retry flaky tests in CI only; failures locally should surface immediately.
    retry: IS_CI ? 2 : 0,
    setupFiles: [
      resolve(dirname, './tests/setup/polyfills.ts'),
      resolve(dirname, './tests/vitestSetup.ts'),
      resolve(dirname, './tests/setup/radix.js'),
    ],
    // Don't look for tests in the nextjs output directory
    exclude: [...configDefaults.exclude, `.next/*`, /* ... */],
    reporters: [['default']],
    coverage: {
      reporter: ['text', 'text-summary', 'lcov'],
      include: ['lib/**/*.ts'],
    },
  },
})

这里有个值得注意的并发配套策略:并行执行放大了 flaky 测试的暴露面(同一文件在不同 worker、不同时序下跑),所以 studio 配置用 retry: IS_CI ? 2 : 0 做了区分——CI 上重试 2 次以吸收时序抖动,本地保持 0 重试让失败立刻暴露。这是并行化测试中"稳定性与真实性"权衡的典型做法。

apps/docs/vitest.config.ts 则展示了另一个并行相关的要点——globalSetup

test: {
  exclude: ['examples/**/*', '**/node_modules/**'],
  setupFiles: ['vitest.setup.ts'],
  globalSetup: ['vitest.globalSetup.ts'],
}

从源码结构看,globalSetup 在整个测试进程只跑一次(共享外部资源,如启动 mock server),而 setupFiles 在每个测试文件/隔离环境里各自执行。当你为并发测试引入共享资源(数据库、端口)时,这个"一次 vs 每次"的语义边界决定了你的资源该放在哪一层初始化。

packages/ai-commands/vitest.config.ts 给 AI 调用类测试放宽了超时:

test: {
  environment: 'node',
  testTimeout: 30000,
  setupFiles: ['./vitest.setup.ts'],
}

testTimeout: 30000 并非并发配置,但与并发强相关:当多个测试并发发起真实 LLM 请求时(见第 4 节实例),单个请求排队 + 网络延迟很容易超过默认 5 秒超时,放宽超时是并发化此类测试的必要配套。

3. 文件内并发:test.concurrentdescribe.concurrent

3.1 单个测试并发

// Individual concurrent tests
test.concurrent('test 1', async ({ expect }) => {
  expect(await fetch1()).toBe('result')
})

test.concurrent('test 2', async ({ expect }) => {
  expect(await fetch2()).toBe('result')
})

// All tests in suite concurrent
describe.concurrent('parallel suite', () => {
  test('test 1', async ({ expect }) => {})
  test('test 2', async ({ expect }) => {})
})

test.concurrent 让这一条测试与同文件内其他测试并发执行;describe.concurrent 则把整个 suite 下的测试全部标记为并发。

关键约束(参考文档反复强调):并发测试中必须使用测试上下文里的 { expect },而不是从 vitest 顶层 import 的 expect 原因是每个并发测试拥有独立的执行上下文(fixture、mock 状态),上下文注入的 expect 会绑定到当前测试,避免断言错误地落到别的并发测试上。

3.2 本仓库的真实用例

packages/ai-commands/src/sql/functions.test.ts 正是参考文档所描述模式的落地实现——两个并发发起 OpenAI 请求的 SQL 调试测试:

import { describe, expect, test } from 'vitest'

describe('debug', () => {
  test.concurrent('fix order of operations', async ({ expect }) => {
    const { sql } = await debugSql(
      openai,
      'relation "departments" does not exist',
      codeBlock`...`
    )
    expect(formatSql(sql)).toMatchSnapshot()
  })

  test.concurrent('fix typos', async ({ expect }) => {
    const { sql, solution } = await debugSql(
      openai,
      'syntax error at or near "fromm"',
      codeBlock`
      select * fromm employees;
    `
    )
    expect(solution).toBeDefined()
    expect(formatSql(sql)).toMatchSnapshot()
  })
})

注意三个细节与文档完全对应:一是 describe 保持普通(顺序调度),只把两个 I/O 密集的 test 标记为 .concurrent,因为二者相互独立、无共享状态,天然适合并发;二是严格使用上下文参数 async ({ expect });三是配套的 testTimeout: 30000(见 2.2 节)保证了并发等待时不会误超时。这类"外部 I/O 密集 + 相互独立"的场景,正是文件内并发收益最大的地方。

4. 在并发上下文中强制顺序:sequential 修饰符

当 suite 整体并发的同时,个别测试又依赖顺序或共享状态时,用 test.sequential / describe.sequential 把它们"钉"回串行:

describe.concurrent('mostly parallel', () => {
  test('parallel 1', async () => {})
  test('parallel 2', async () => {})

  test.sequential('must run alone 1', async () => {})
  test.sequential('must run alone 2', async () => {})
})

// Or entire suite
describe.sequential('sequential suite', () => {
  test('first', () => {})
  test('second', () => {})
})

语义上可以理解为:concurrentsequential 是同一调度维度上的两个极性,默认极性是顺序执行,.concurrent 把默认极性翻转,.sequential 则可以在翻转后的环境中把个别测试拉回默认极性,反之亦然。实践上最常见的组合就是参考文档所示的"大并发 + 小串行"——大部分测试独立可并行,少数涉及共享状态(如数据库事务、全局单例)的测试显式串行化。

5. 并发度上限与隔离

5.1 maxConcurrency

defineConfig({
  test: {
    maxConcurrency: 5, // Max concurrent tests per file
  },
})

maxConcurrency 限制单个文件内同时运行的测试数,默认是 5。如果你的并发测试都打真实后端(如同 3.2 节打 OpenAI 的测试),这个值实际上就是你对该后端的并发请求数上限,调它等于调对下游服务的压力。

5.2 文件隔离(Isolation)

Vitest 默认让每个测试文件跑在隔离的环境中(模块图、全局状态、mock 注册彼此独立)。这是并行安全的根基,也是开销来源(每个文件都要重建环境):

defineConfig({
  test: {
    // Disable isolation for faster runs (less safe)
    isolate: false,
  },
})

isolate: false 可以让同一 worker 内连续运行的文件复用环境,显著提速,但代价是前一个文件泄漏的全局状态可能被后一个文件看到——一旦有测试污染了 globalThis 或第三方库单例,故障会呈现出与文件执行顺序强相关的"灵异"行为。参考文档把这一点直接标注为 "less safe",本仓库所有子包也均未关闭隔离,保持了默认安全配置。从源码结构看,只有当你确认测试彼此无状态耦合、且已用第 7 节的 shuffle 验证过顺序无关性时,关闭隔离才是安全的提速手段。

6. 分片(Sharding):把测试摊到多台机器

6.1 基础用法

--shard=i/n 让 Vitest 按稳定算法把文件集合切 n 份,只跑第 i 份:

# Machine 1
vitest run --shard=1/3

# Machine 2
vitest run --shard=2/3

# Machine 3
vitest run --shard=3/3

三台机器各自只承担 1/3 的文件,墙钟时间约降为 1/3,且每个分片的文件分配是可复现的。

6.2 CI 分片:GitHub Actions 矩阵 + 报告合并

参考文档给出的完整 CI 方案是"分片执行 + blob 报告 + 汇总合并"三步:

jobs:
  test:
    strategy:
      matrix:
        shard: [1, 2, 3]
    steps:
      - run: vitest run --shard=${{ matrix.shard }}/3 --reporter=blob

  merge:
    needs: test
    steps:
      - run: vitest --merge-reports --reporter=junit

配合 blob reporter 的命令行形态:

# Each shard outputs blob
vitest run --shard=1/3 --reporter=blob --coverage
vitest run --shard=2/3 --reporter=blob --coverage

# Merge all blobs
vitest --merge-reports --reporter=json --coverage

工作流要点:每个分片用 --reporter=blob 把结果(含覆盖率)序列化落盘;最后一个 merge job 依赖全部 test job 完成后,用 --merge-reports 把各分片的 blob 汇总成一份完整报告(可输出 json/junit 等格式),供 CI 判定与归档。这套模式让"结果完整"与"执行并行"两全:任何一个分片失败都能被合并后的总报告捕获。

7. 测试顺序控制:sequence 与 Shuffle

7.1 sequence 配置

defineConfig({
  test: {
    sequence: {
      // Run tests in random order
      shuffle: true,

      // Seed for reproducible shuffle
      seed: 12345,

      // Hook execution order
      hooks: 'stack', // 'stack', 'list', 'parallel'

      // All tests concurrent by default
      concurrent: true,
    },
  },
})

参数含义:

  • shuffle: true:文件与测试按随机顺序执行,是发现隐藏顺序依赖(测试 A 污染了测试 B 依赖的状态)最有效的探针;
  • seed:随机种子。固定 seed 后 shuffle 结果可复现——CI 上报了 shuffle 导致的失败,本地用同一 seed 即可稳定复现,排查完再换 seed 继续"随机轰炸";
  • hooks:控制 beforeAll/beforeEach 等 hook 的执行顺序,'stack'(默认,后注册的先执行,与嵌套语义一致)、'list'(注册顺序执行)、'parallel'
  • concurrent:在 sequence 层面全局开启并发,效果类似给所有测试加 .concurrent,属于激进配置,仅在确认无共享状态时使用。

7.2 局部 shuffle

不想要全局随机时,可以只对单个 suite 或一次运行开 shuffle:

// Via CLI
vitest --sequence.shuffle

// Per suite
describe.shuffle('random order', () => {
  test('test 1', () => {})
  test('test 2', () => {})
  test('test 3', () => {})
})

describe.shuffle 只打乱该 suite 内测试的顺序,适合"怀疑某几个测试互相依赖"的定向排查;vitest --sequence.shuffle 则是对一次本地运行做整体随机验证。

8. Pool 选型:threads / forks / vmThreads

pool 决定 worker 的技术载体,三种选项的特性与配置如下:

8.1 threads(默认)

基于 worker_threads,创建快、共享进程内存,是默认选择:

defineConfig({
  test: {
    pool: 'threads',
    poolOptions: {
      threads: {
        maxThreads: 8,
        minThreads: 2,
        isolate: true,
      },
    },
  },
})

isolate: true(默认)时每个文件在独立 worker 实例中运行(即 5.2 节说的文件隔离),maxThreads/minThreads 等价于该 pool 内的 worker 上下限。

8.2 forks

子进程隔离,进程边界最硬——native 模块崩溃、C++ 扩展污染等问题在线程池里会互相传染,切到 forks 即可消除;代价是进程创建与通信开销更大:

defineConfig({
  test: {
    pool: 'forks',
    poolOptions: {
      forks: {
        maxForks: 4,
        isolate: true,
      },
    },
  },
})

参考文档的定性很直白:"Better isolation, slower"。选型建议:纯 TS/JS 的库和前端应用(如本仓库的 studio、ui 包)保持默认 threads 即可;一旦测试加载了 native 依赖或出现线程间幽灵污染,优先尝试 forks。

8.3 vmThreads

每个文件跑在独立的 Node vm 上下文里,隔离粒度最细(文件间连 globalThis 都不共享),适合"同一个文件内多个环境都要干净"的极端场景:

defineConfig({
  test: {
    pool: 'vmThreads',
  },
})

9. --bail:失败即停

在调试阶段,并行 + 全量跑会淹没第一个真正的错误,--bail 让测试在累计 N 次失败后停止:

vitest --bail 1    # Stop after 1 failure
vitest --bail      # Stop on first failure (same as --bail 1)

--bail 1 是最常用的调试姿态:第一个失败即终止后续调度,把并行运行降级为"快速定位首个坏点",修完再放开。

10. 要点速查与落地建议

参考文档末尾的 Key Points,结合本仓库实践归纳如下:

能力 开关 / API 默认行为 适用场景
文件级并行 fileParallelism true,自动启用 所有仓库默认受益
worker 数量 maxWorkers / minWorkers 按 CPU 推断 限制 CI 容器内存占用
文件内并发 test.concurrent / describe.concurrent 关闭,需显式声明 独立 I/O 密集测试(如 functions.test.ts
并发中强制串行 test.sequential / describe.sequential 共享状态的少数测试
文件内并发上限 maxConcurrency 5 控制对下游服务的并发压力
文件隔离 isolate true,建议保持 追求极致速度且确认无状态耦合时才关
分片 vitest run --shard=i/n CI 多机/多 job 分摊
报告合并 --reporter=blob + --merge-reports 分片结果汇总为单一报告
顺序随机化 sequence.shuffle / seed / describe.shuffle 关闭 挖掘隐藏顺序依赖
执行池 pool: 'threads' | 'forks' | 'vmThreads' threads 按隔离需求与性能权衡选型
失败即停 --bail [n] 关闭 调试阶段快速定位

给本仓库(或同构 pnpm + Vitest monorepo)的实操建议:

  1. 什么都不配也能并行:文件级并行默认开启,本仓库七个 vitest.config.ts 均未显式配置 fileParallelism/pool,即全部运行在默认 threads 并行之下;
  2. 并发标注要保守:只对"相互独立 + I/O 密集"的测试加 .concurrent,并一律使用上下文 expect——packages/ai-commands 的用例是标准范本;
  3. 用 shuffle 验证隔离假设:定期 vitest --sequence.shuffle 跑一轮,若失败率上升,说明存在顺序依赖,回到 isolate: true 语义下逐个隔离修复;
  4. flaky 治理靠 retry 分层:参考 studio 配置retry: IS_CI ? 2 : 0,把抖动吸收留在 CI,本地保持零容忍。

以上配置与 API 均出自技能参考文档 features-concurrency.md(由 Vitest 官方文档生成,基于 Vitest 3.x),并已在当前仓库的 pnpm-workspace.yamlvitest: ^4.1.4)与各子包配置中验证了实际应用形态;具体默认值随版本可能有差异,落地时以你所安装版本的官方文档为准。

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

项目优选

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