
很多测试不稳定,并不是断言写错了,而是测试数据没有边界:上一条用例留下的用户还在库里,固定邮箱被并发任务重复创建,CI 里偶尔多一条历史订单,结果同一套测试今天过、明天红。测试数据管理的目标不是把数据造得越像生产越好,而是让每条用例都能清楚说明自己依赖什么、创建什么、清理什么。
先把数据分层
测试数据常见有三种来源。它们适合解决不同问题,混在一起就会越来越难维护。
| 类型 | 适合场景 | 风险 |
|---|---|---|
| Fixture | 少量稳定事实,例如角色、地区、配置项 | 文件变大后牵一发动全身 |
| Factory | 按需创建用户、订单、项目等业务对象 | 默认值不合理会掩盖边界条件 |
| Seed | 本地演示或端到端环境的基础数据 | 容易被误用成单元测试依赖 |
一个实用规则是:Fixture 放“长期不变的事实”,Factory 放“每条测试自己需要的对象”,Seed 只服务环境启动和人工验证。测试用例里如果看不出数据从哪里来,后续排查就会很痛苦。
Fixture 要小而稳定
Fixture 的价值在于可读和稳定,不在于覆盖所有业务状态。比如权限系统可以保留一份最小角色集,让测试明确引用 admin、viewer,而不是每条用例都重新拼装角色。
1 | { |
维护 Fixture 时要避免两个坏味道:第一,把生产导出的匿名数据整包塞进仓库,导致用例依赖一堆无关字段;第二,在多个测试之间悄悄修改共享对象,导致执行顺序影响结果。Fixture 应该像接口契约一样保守变更,新增字段可以,改变含义要进入评审。
Factory 负责按需构造

业务对象变化快时,Factory 比手写大段 JSON 更耐用。它提供合理默认值,并允许用例覆盖关键字段。这样测试只暴露和场景有关的差异,其他字段由工厂兜底。
1 | let seq = 0; |
这里的重点是确定性。不要在默认值里使用真实当前时间、随机邮箱或外部服务返回值。需要唯一性时,用可预测的序号;需要时间时,固定时钟或显式传入。测试失败后,开发者应该能复现同一个输入,而不是猜测随机数当时生成了什么。
隔离比清理更重要

清理数据当然必要,但更稳的思路是先隔离。只依赖 afterEach 删除数据,遇到进程崩溃、超时中断或并发执行时很容易留下脏状态。不同测试层级可以选择不同隔离策略。
| 测试层级 | 推荐隔离方式 | 说明 |
|---|---|---|
| 单元测试 | 内存对象或临时目录 | 不接触共享数据库 |
| 集成测试 | 每条用例事务回滚 | 快,适合同进程串行执行 |
| API 测试 | 独立 schema 或命名前缀 | 适合并发跑多组测试 |
| E2E 测试 | 专用环境加数据重置任务 | 成本高,只覆盖关键路径 |
如果数据库支持事务,集成测试可以在用例开始时开启事务,结束时统一回滚。对于需要真实提交的场景,例如消息队列消费或后台任务,可以给本轮测试生成唯一命名前缀,再按前缀清理。核心原则是:测试之间不能通过共享数据“对话”。
让 CI 失败可诊断
CI 里的测试数据还要考虑并发和重跑。构造数据时,把构建号、分片号或测试运行 ID 放进命名前缀,能避免两个任务抢同一个账号。失败时保留最小诊断信息,例如本轮创建了哪些实体、外部 ID 是什么、清理步骤是否成功。日志里不要打印真实密钥或用户隐私,但测试环境里的对象标识应该足够定位问题。
还要避免把清理逻辑藏在本地脚本里。CI、开发机和临时调试环境应该复用同一套 reset 命令,最好做到可重复执行:
1 | npm run test:db:reset |
如果 reset 第一次失败、第二次也失败,它应该给出明确错误,而不是悄悄跳过部分表。
落地检查表
上线或重构测试套件前,可以按这份清单过一遍:
| 检查项 | 判断标准 |
|---|---|
| 数据来源 | 用例能看出依赖 Fixture 还是 Factory |
| 默认值 | Factory 默认值稳定、可预测、无真实外部依赖 |
| 唯一性 | 并发执行时不会抢同一个邮箱、订单号或资源名 |
| 隔离 | 测试失败或中断后不会污染下一条用例 |
| 清理 | reset 命令可重复执行,失败信息明确 |
| 诊断 | CI 日志能定位本轮创建的数据和清理结果 |
测试数据一旦失控,团队会慢慢失去对自动化测试的信任。把 Fixture 做小,把 Factory 做稳,把隔离策略写进测试基础设施,测试失败时就更容易指向真实问题,而不是让人先怀疑环境是不是又脏了。