Vitest 并行跑测试,行消失了——fileParallelism 与共享 DB 行的竞争陷阱
单文件跑测试,全绿。整套跑,稳定炸 4 个。报错是 expected null to be '{"cookies":[...]}' 或者 expected 404 to be 200——数据明明刚写进去,下一行断言就消失了。
直觉第一反应是"异步时序问题",于是加 await、加 sleep,折腾半天无果。直到把两个出问题的测试文件放在一起单独跑,才复现——问题出在文件之间,不是文件内部。
现象
npx vitest run path/to/A.test.ts→ 9/9 PASSnpx vitest run path/to/B.test.ts→ 9/9 PASSnpx vitest run(全套)→ 4/9 FAIL,报AssertionError: expected null to be '...'
失败的测试散落在两个不同模块的测试文件里,表面上毫无关联。
根因
Vitest 默认 fileParallelism: true——多个测试文件并发执行。
项目里两个测试文件恰好都依赖同一条「种子行」(一个管理员账号,由初始化脚本写入,所有文件复用同一个 ADMIN_USER_ID)。
- 文件 A(
cookies-store.test.ts):beforeEach里执行hardDelete(ADMIN_USER_ID)清理现场 - 文件 B(
credentials.test.ts):先PUT写入 cookies,再GET断言内容存在
两文件并发运行时,时序如下:
文件 B: PUT /credentials → 写入 cookies 行 ✅
文件 A: beforeEach → DELETE WHERE user_id = ADMIN_USER_ID 💥
文件 B: GET /credentials → 读到 null → AssertionError ❌
文件 A 的清理逻辑在文件 B 的断言之前执行,将刚写入的行删掉了。两边都"正确",却互相踩踏。
直觉上会想到 pool: 'forks' + poolOptions.forks.singleFork: true——把所有文件放进同一个 fork 里跑。但这只改变了进程隔离,文件之间仍然并发执行。beforeEach 依然会与另一个文件的请求交错,竞态条件不消失。
真正需要的是文件串行执行,不是进程合并。
正解
在项目根的 vitest.config.ts 中加一行:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
fileParallelism: false,
},
})fileParallelism: false 会让 Vitest 按顺序逐文件执行,文件 A 跑完再跑文件 B,beforeEach 永远不会在另一个文件的测试中途触发。
加完配置后:
npx vitest run
# 9/9 PASS ✅一行配置解决整类竞态。代价是测试套件墙钟时间约变慢 2 倍,但完全消除「跨文件共享行」这一类 flake。
将 ADMIN_USER_ID 从全局种子改为每个文件用 runId-suffixed UUID 自己插入、afterAll 自己清理。好处是文件可以重新开启并行。代价是每个文件要写 ~30 行测试基础设施(FK 链插入 + teardown)。
对于只需要 1 个管理员账号的小项目,fileParallelism: false 是 ROI 最高的选择。
一句话外卖
测试单跑全绿、合跑必炸——先查「是否有多个文件共用同一条数据库行」,
fileParallelism: false是最小代价的止血手段。