Skip to main content
bun test 有三个彼此独立的开关,用于同时运行多个项目: 它们可以组合使用:一个 CI 任务可以运行 bun test --shard=2/4 --parallel,而该分片中的文件仍然可以包含 test.concurrent 测试

--parallel

terminal
主 bun test 进程会变成一个协调器。它照常发现测试文件,然后启动工作进程,并一次向每个进程分配一个文件。每个测试完成后,结果会流式返回,因此输出看起来与串行运行相同。协调器会将每个文件的结果放在其文件名下方一起打印,并且绝不会将某个测试的 console.log 输出与另一个文件的输出交错显示。
工作进程会惰性启动。第一个工作进程会立即启动;只有当每个正在运行的工作进程都持续忙碌几毫秒后,协调器才会启动其余进程(--parallel-delay=<ms>,默认值为 5)。因此,一组很小的文件会在单个工作进程中运行,不会产生启动进程的开销;而第一个耗时较长的文件会触发全部工作进程启动。

文件如何分配

协调器会按路径对文件排序,并将文件拆分为每个工作进程各自连续的一块,因此位于同一目录中的文件通常会落在同一个进程中,因为它们一般会导入相同的模块(分块边界可能落在目录中间,被转移的文件也会移动)。当某个工作进程处理完自己的文件块后,它会从另一个工作进程剩余的最大文件块中取走后半部分。使用 --timings 时,协调器会按记录的耗时而非文件数量切分文件块;每个工作进程会先启动耗时最长的文件,而空闲的工作进程会从剩余时间最多的文件块中取走尚未启动且耗时最长的文件。

每个文件都会被隔离(除非选择退出)

--parallel 隐含启用 --isolate:即使两个文件落在同一个工作进程上,每个文件也会在全新的全局对象中运行。使用 --parallel 通过的测试不会依赖前一个文件泄漏的状态 --parallel --no-isolate 会关闭此功能:每个工作进程会为分配给它的所有文件保留一个全局对象和一个模块注册表,就像串行运行 bun test 时整个测试套件的行为一样。每个工作进程只会求值一次导入内容(以及 --preload 模块),而不是每个文件都求值一次,这是运行大量小文件测试套件最快的方式。代价是某个文件可能会观察到同一工作进程上较早运行的文件遗留下来的内容。预加载级别的 beforeAll/afterAll 钩子仍会包裹每个文件,因为工作进程无法知道哪个文件是它要运行的最后一个文件。

工作进程环境

每个工作进程都会将 BUN_TEST_WORKER_ID 和 JEST_WORKER_ID 设置为从 1 开始的索引,因此测试可以为每个工作进程选择不同的数据库、端口范围或临时目录:
db.test.ts
影响测试执行方式的标志(--timeout、--preload、--define、--coverage、--update-snapshots、-t、--retry、--rerun-each、--concurrent、--randomize/--seed,……)都会转发给工作进程。协调器会按文件粒度处理 --bail:达到失败阈值后,它不会再启动新文件,但已经在运行的文件会继续完成。 协调器会合并覆盖率、JUnit XML 和快照写入,因此 --parallel --coverage --reporter=junit --reporter-outfile=junit.xml 会生成一份报告。 如果工作进程崩溃(某个测试调用了 process.exit),协调器会将该工作进程当时正在运行的文件报告为失败,并由替代工作进程接手剩余文件。致命信号导致的崩溃会中止整个运行,因此后续通过的文件无法掩盖它。有效工作进程只有一个时(--parallel=1,或测试套件只有一个测试文件),bun test 会在主进程中运行文件,因此 process.exit 会以该退出代码结束整个运行。

--parallel 何时有帮助,何时没有

当测试套件的主要耗时来自测试执行时,--parallel 会有明显收益,例如 I/O 等待、实际计算、子进程或大量文件。但它也有开销:每个文件都会在全新的全局对象中重新求值其导入内容(参见 --isolate),而且每个工作进程都是独立进程,都需要单独进行 JIT 预热。对于一组运行非常快且都导入同一个大型模块图的文件,普通的 bun test(一个进程,共享一个模块注册表)可能更快。两种方式都试试;Bun 会在每次运行结束时打印耗时数据。

--isolate

terminal
在同一个进程内的全新 JavaScript 全局对象中运行每个测试文件。在文件之间,Bun 会:
  • 创建新的 globalThis(因此文件附加到 globalThis 上的属性、被修补的内置对象以及模块级状态都会消失),
  • 清除 ESM 和 CommonJS 模块注册表(每个文件都会重新求值其导入内容),
  • 关闭文件留下的服务器、套接字、文件监视器和子进程,取消其计时器,并恢复 fake timers,
  • 在新的全局对象中重新运行 --preload 脚本
Jest 和 Vitest 默认会隔离每个文件。这样可以消除“单独运行时通过、在完整测试套件中失败”的问题,代价是每个文件都要重新求值导入内容。 为了尽可能降低这项开销,Bun 会在进程级别缓存转译后的源代码和字节码,并在多个全局对象之间共享这些缓存。第二个导入某个模块的文件无需再读取、转译和解析该模块,可以直接进入求值阶段。只有模块顶层代码会再次运行。 不使用 --isolate 时(默认情况),所有文件共享一个全局对象和一个模块注册表。这是最快的模式,适用于文件之间不会泄漏状态的测试套件

文件内的并发测试

--parallel 会将 文件 分散到各个核心上。在一个文件内,测试仍然会逐个运行,除非选择启用并发;启用后,某个 async 测试等待 I/O 时,其他测试可以与其重叠运行:
api.test.ts
  • test.concurrent(...)/describe.concurrent(...) 标记单个测试或整个测试组
  • --concurrent 会将每个测试视为并发测试;test.serial 可退出并发模式
  • --max-concurrency=N 限制同时运行的测试数量(默认值为 20)
  • bunfig.toml 中的 concurrentTestGlob 仅对匹配的文件启用该功能
并发测试共享同一个线程和全局对象;这是面向 I/O 密集型测试的协作式并发,而不是额外的 CPU 核心。在并发执行时,expect.assertions() 和其他每个测试的全局状态需要谨慎处理——请参阅并发测试执行

使用 --shard 将测试套件拆分到 CI 机器

terminal
每台机器都会按路径对发现的测试文件排序,并获取一个确定性的分片,因此所有分片合起来会在无需协调的情况下恰好覆盖每个文件一次。不使用 --timings 时,排序列表中的第 i 个文件会分配给分片 (i mod n) + 1——按文件数量平衡,而不是按文件耗时平衡

使用 --timings 进行平衡

文件数量并不能很好地代表耗时:某个分片可能会分到所有耗时较长的集成测试。为 bun test 提供每个文件的耗时记录后,它会改为按总耗时切分分片,并将相邻文件(它们共享导入内容)放在一起:
terminal
该文件是普通 JSON,按耗时从高到低排列,因此也可以作为“哪些内容较慢”的报告:
.bun-test-timings.json
  • 路径相对于项目根目录;值表示整个文件的墙钟耗时,单位为毫秒。
  • 不使用 --shard 时,--update-timings 会将结果合并到读取的内容中,因此在本地重新运行部分测试套件会刷新相应条目并保留其余条目。已不存在文件的条目会保留;删除该文件即可重新开始。
  • 使用 --shard 时,--update-timings 只会写入该分片运行过的文件——见下文。
  • 切分分片时,Bun 会假设没有条目的文件耗时为中位数;使用 --parallel 时,则会先启动这些文件。
  • 使用 --timings 时,--parallel 也会使用这些耗时:协调器会按耗时切分工作进程文件块,每个工作进程会先启动耗时最长的文件。

每个分片一个 timings 文件

可以多次传入 --timings。Bun 会将这些文件作为一个表读取,并跳过尚不存在的路径。--update-timings 会写入到第一个路径。使用 --shard 时,该输出只包含该分片运行过的文件,因此各分片的输出互不重叠。下次运行时将这些文件一起读取,它们就能组成完整的测试套件,无需合并步骤。主页面上的大型代码库包含完整的 CI 工作流。

对比

2,000 个 TypeScript 测试文件 × 每个文件 8 个小型测试,所有文件都导入一个基于 zod、date-fns 和 lodash 构建的小型应用,并通过每个 runner 的预加载机制加载一个共享 setup 文件(自定义 matcher + beforeEach/afterEach)——这就是大型应用单元测试套件的形态(bench/test/app、bun app/setup.ts 2000 20)。16 核 Apple M4 Max:
墙钟时间,使用 hyperfine --warmup 1,Bun 1.4,Node.js 25.6,每个 runner 使用默认配置及其 setup 文件选项 (bunfig.toml test.preload、setupFilesAfterEnv、setupFiles)。生成器和配置文件都在代码库中, 因此可以重新运行。测试内容会影响比例:此测试套件有意让每个文件的开销占主导,而不是测试本身。
耗时分布情况:每个文件都使用全新全局对象时,每个 runner 都会重新求值 2 000 次导入内容和 setup 文件。Bun 会在这些全局对象之间共享转译后的源代码和字节码,因此无需重新解析,但模块求值和 JIT 预热仍会在每个文件中重复进行。这些重复工作正是这种情况下一个共享全局对象(bun test)胜过十六个隔离工作进程,而十六个共享全局对象(--parallel --no-isolate)又胜过前两者的原因。