很多测试变得难维护,不是因为断言不会写,而是因为测试数据越来越乱。开始大家只是在测试里手写两个对象,后面对象字段一多、状态一复杂、上下游依赖一多,就会出现同样的坏味道:几十个测试各自复制一份数据,改一个字段要全仓搜,某些对象甚至组合出了业务里根本不可能出现的状态。
TypeScript 在测试数据治理里最大的价值,是把“可构造什么状态”这件事也做成显式规则。Builder、fixture 和 mock 并不是三种可互换工具,而是分别解决三种不同问题:场景默认值、可复现输入和依赖替身。把这三者混用,测试会越来越脆;把它们拆开,维护成本会明显下降。
先把三种东西分清:Builder、Fixture、Mock 各负责什么
| 工具 | 主要作用 | 最适合的场景 |
|---|---|---|
| Builder | 生成带默认值、可按需覆写的业务对象 | 领域对象、表单输入、状态数据 |
| Fixture | 固定一组可复现的完整样本 | 回归测试、快照测试、跨系统协议样本 |
| Mock | 替代外部依赖的行为 | API、数据库、第三方服务、时间或随机数 |
最常见的问题,是把它们全都叫“测试数据”,然后在同一个测试里既复制对象,又手写假函数,再顺手塞一个不完整 fixture。这样做的后果通常不是“多写一点”,而是职责全乱了。
Builder 的价值,在于给场景一个稳定起点,而不是让你随便拼对象
一个健康的 Builder 通常有两个特征:
- 默认值代表一个真实且可用的业务状态。
- 覆写入口只允许改变合理字段,而不是把所有属性都开放成
Partial<any>。
type User = {
id: string
name: string
role: 'admin' | 'editor' | 'viewer'
active: boolean
}
function buildUser(overrides: Partial<User> = {}): User {
return {
id: 'user-001',
name: 'Alice',
role: 'viewer',
active: true,
...overrides
}
}
这个模式简单,但很有效:大部分测试只关心一两个字段时,不需要每次重新写完整对象。更重要的是,默认值形成了“基准状态”,团队不会再每个测试各自发明一套初始世界。
Partial 虽然方便,但它经常把“不可能状态”悄悄引进测试
很多项目会进一步把 Builder 做成 buildOrder(overrides: Partial<Order>),然后觉得已经足够灵活。问题在于,Partial 本身只代表“字段可选”,并不代表“业务状态合法”。
举个例子:
- 订单
status是paid,但paidAt却是null - 表单
submitted是true,但errors里还留着必填缺失
这类数据在测试里很常见,因为手工拼对象时没人会顺手维护所有关联字段。更稳妥的做法是给关键状态提供场景化 Builder,而不是把一切交给通用覆写:
function buildPaidOrder(overrides: Partial<Order> = {}): Order {
return {
...buildOrder(),
status: 'paid',
paidAt: '2026-06-08T10:00:00Z',
...overrides
}
}
Fixture 适合“固定样本”,不适合承担所有变体
Fixture 的优势是稳定、完整、可复现,尤其适合:
- 回归测试需要复现实战输入
- 与外部系统协同的 JSON 样本
- 大对象快照或协议对照
但 fixture 不适合承担所有微小变体。如果一个场景只想改其中 1 个字段,却每次都复制一整份 fixture,很快会出现多份近似样本漂移。更稳的方式通常是:fixture 负责固定底座,Builder 负责生成小变体。
Mock 的重点,不是“返回什么”,而是“边界行为是否一致”
Mock 最容易被误用成“随便返回点能过测试的数据”。但真正重要的是:
- 调用参数是否被正确记录或校验。
- 返回结果是否符合依赖的真实契约。
- 失败分支、重试分支和超时分支是否都被覆盖。
如果 mock 只关注 happy path,测试就很容易看似很多,实际上对风险边界没有任何帮助。
一个常见失败案例:测试数据工厂变成万能对象打印机
某团队最初做 Builder 是为了解决重复对象,后来为了“通用”,把所有对象都做成 buildX(overrides: DeepPartial<X>)。短期确实很省事,长期却让问题更严重:
- 任何字段都能被覆盖,导致测试随手构造不合法状态。
- 阅读测试的人很难看出这个场景真正依赖哪些关键字段。
- 一旦领域模型变化,Builder 自己也因为过度泛化而越来越难维护。
Builder 最怕的不是字段少,而是责任模糊。它应该帮助测试表达场景,不应该成为绕开场景规则的后门。
一份测试数据治理清单
- Builder 是否提供真实且稳定的默认场景,而不是空对象拼接。
- 关键业务状态是否有专门场景 Builder,而不是全靠
Partial临时拼。 - Fixture 是否只用于固定样本,而不是复制出一堆近似变种。
- Mock 是否覆盖了失败和边界行为,而不只是 happy path。
- 新增字段后,测试数据是否有统一更新入口,而不是分散在几十处手写对象里。
总结
测试数据治理本质上也是类型治理。Builder 负责表达场景默认值,fixture 负责固定可复现样本,mock 负责替代边界行为。只要职责拆清,TypeScript 测试就不会再因为“数据怎么准备”而越来越难改,反而会在对象变复杂后体现出更高的可维护性。
本系列导航:
- 如果你要把字段错误和业务状态一起建模,接着读 TypeScript 表单与错误状态建模
- 若你发现测试数据和边界输入在漂移,再看 TypeScript 运行时校验与静态类型协作
- 如果你想把抽象策略也带进测试可维护性里,再读 TypeScript 设计模式实战
延伸阅读:


