把测试数据管住:Fixtures、Factory 与隔离清理清单

测试数据管理

很多测试不稳定,并不是断言写错了,而是测试数据没有边界:上一条用例留下的用户还在库里,固定邮箱被并发任务重复创建,CI 里偶尔多一条历史订单,结果同一套测试今天过、明天红。测试数据管理的目标不是把数据造得越像生产越好,而是让每条用例都能清楚说明自己依赖什么、创建什么、清理什么。

先把数据分层

测试数据常见有三种来源。它们适合解决不同问题,混在一起就会越来越难维护。

类型 适合场景 风险
Fixture 少量稳定事实,例如角色、地区、配置项 文件变大后牵一发动全身
Factory 按需创建用户、订单、项目等业务对象 默认值不合理会掩盖边界条件
Seed 本地演示或端到端环境的基础数据 容易被误用成单元测试依赖

一个实用规则是:Fixture 放“长期不变的事实”,Factory 放“每条测试自己需要的对象”,Seed 只服务环境启动和人工验证。测试用例里如果看不出数据从哪里来,后续排查就会很痛苦。

Fixture 要小而稳定

Fixture 的价值在于可读和稳定,不在于覆盖所有业务状态。比如权限系统可以保留一份最小角色集,让测试明确引用 adminviewer,而不是每条用例都重新拼装角色。

1
2
3
4
5
6
{
"roles": [
{ "key": "admin", "permissions": ["project:read", "project:write"] },
{ "key": "viewer", "permissions": ["project:read"] }
]
}

维护 Fixture 时要避免两个坏味道:第一,把生产导出的匿名数据整包塞进仓库,导致用例依赖一堆无关字段;第二,在多个测试之间悄悄修改共享对象,导致执行顺序影响结果。Fixture 应该像接口契约一样保守变更,新增字段可以,改变含义要进入评审。

Factory 负责按需构造

Factory 构造测试数据

业务对象变化快时,Factory 比手写大段 JSON 更耐用。它提供合理默认值,并允许用例覆盖关键字段。这样测试只暴露和场景有关的差异,其他字段由工厂兜底。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let seq = 0;

export function buildUser(overrides = {}) {
seq += 1;

return {
id: `user_${seq}`,
email: `user_${seq}@example.test`,
status: "active",
plan: "free",
createdAt: new Date("2026-01-01T00:00:00Z"),
...overrides
};
}

test("冻结用户不能创建项目", async () => {
const user = await users.insert(buildUser({ status: "locked" }));

await expect(createProject(user.id)).rejects.toThrow("USER_LOCKED");
});

这里的重点是确定性。不要在默认值里使用真实当前时间、随机邮箱或外部服务返回值。需要唯一性时,用可预测的序号;需要时间时,固定时钟或显式传入。测试失败后,开发者应该能复现同一个输入,而不是猜测随机数当时生成了什么。

隔离比清理更重要

测试隔离与 CI 并发

清理数据当然必要,但更稳的思路是先隔离。只依赖 afterEach 删除数据,遇到进程崩溃、超时中断或并发执行时很容易留下脏状态。不同测试层级可以选择不同隔离策略。

测试层级 推荐隔离方式 说明
单元测试 内存对象或临时目录 不接触共享数据库
集成测试 每条用例事务回滚 快,适合同进程串行执行
API 测试 独立 schema 或命名前缀 适合并发跑多组测试
E2E 测试 专用环境加数据重置任务 成本高,只覆盖关键路径

如果数据库支持事务,集成测试可以在用例开始时开启事务,结束时统一回滚。对于需要真实提交的场景,例如消息队列消费或后台任务,可以给本轮测试生成唯一命名前缀,再按前缀清理。核心原则是:测试之间不能通过共享数据“对话”。

让 CI 失败可诊断

CI 里的测试数据还要考虑并发和重跑。构造数据时,把构建号、分片号或测试运行 ID 放进命名前缀,能避免两个任务抢同一个账号。失败时保留最小诊断信息,例如本轮创建了哪些实体、外部 ID 是什么、清理步骤是否成功。日志里不要打印真实密钥或用户隐私,但测试环境里的对象标识应该足够定位问题。

还要避免把清理逻辑藏在本地脚本里。CI、开发机和临时调试环境应该复用同一套 reset 命令,最好做到可重复执行:

1
2
npm run test:db:reset
npm run test:integration

如果 reset 第一次失败、第二次也失败,它应该给出明确错误,而不是悄悄跳过部分表。

落地检查表

上线或重构测试套件前,可以按这份清单过一遍:

检查项 判断标准
数据来源 用例能看出依赖 Fixture 还是 Factory
默认值 Factory 默认值稳定、可预测、无真实外部依赖
唯一性 并发执行时不会抢同一个邮箱、订单号或资源名
隔离 测试失败或中断后不会污染下一条用例
清理 reset 命令可重复执行,失败信息明确
诊断 CI 日志能定位本轮创建的数据和清理结果

测试数据一旦失控,团队会慢慢失去对自动化测试的信任。把 Fixture 做小,把 Factory 做稳,把隔离策略写进测试基础设施,测试失败时就更容易指向真实问题,而不是让人先怀疑环境是不是又脏了。