用 AI 生成测试数据与夹具
测试数据的成本往往比测试代码本身高:要满足字段约束、要保持多表关联一致、还要能被复现。AI 很适合批量生成这类「结构化但琐碎」的东西,前提是先把三条纪律写进提示词:最小可用、可复现、种子固定。
1. 夹具(fixture)设计:默认值 + 显式覆盖
夹具不要一行行手写,用构造器(builder)把默认值封起来,用例只写「与默认值不同的部分」。
type UserBuilder struct{ u User }
func NewUser() *UserBuilder {
return &UserBuilder{u: User{
ID: 1001,
Name: "张三",
Phone: "13800000000",
Status: StatusActive,
CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
}}
}
func (b *UserBuilder) WithID(id int64) *UserBuilder { b.u.ID = id; return b }
func (b *UserBuilder) WithPhone(p string) *UserBuilder { b.u.Phone = p; return b }
func (b *UserBuilder) Banned() *UserBuilder { b.u.Status = StatusBanned; return b }
func (b *UserBuilder) Build() User { return b.u } // 值返回,避免共享
提示词给它的规格要包含:字段清单与类型、每个字段的合法取值范围、必须满足的唯一约束、以及「未知字段一律不要编造」。
角色:Go 测试工程师。
任务:为 User 与 Order 两张表写夹具构造器。
要求:
1. NewXxx() 返回带全套默认值的 builder,默认值必须落在合法区间内;
2. 每个可变字段提供 WithXxx 方法,Build() 返回值拷贝而不是指针;
3. 默认值固定,不许出现 time.Now() 与未播种的 rand;
4. 关联字段(order.UserID)默认与夹具 ID 对齐,并提供 WithUser 关联覆盖;
5. 需要「异常数据」时另开方法与显式的非法夹具类型(InvalidUser),不要污染默认值。
关键的三个字是值返回:夹具用指针共享,某个用例改了 Status,后面的用例全部受污染,而且报错点离原因很远。
2. 批量造数:脚本化 + 固定种子
需要几百条数据时,写生成函数而不是复制粘贴。
func SeedOrders(n int, seed int64) []Order {
r := rand.New(rand.NewSource(seed)) // 固定种子:同样的 seed 必然同样的数据
out := make([]Order, 0, n)
for i := 0; i < n; i++ {
out = append(out, Order{
ID: int64(10000 + i),
UserID: int64(1001 + r.Intn(50)),
Amount: int64(r.Intn(100000)),
Status: []string{StatusPending, StatusPaid, StatusClosed}[r.Intn(3)],
CreatedAt: baseTime.Add(time.Duration(i) * time.Minute),
})
}
return out
}
Python 侧用 faker 时同样先把种子钉死,再配合「脱敏保留一致性」的辅助函数:
import hashlib
from faker import Faker
Faker.seed(20240101)
fake = Faker("zh_CN")
SALT = "kb-seed"
def pseudonym(uid: str) -> str: # 同一 uid 永远得到同一假名
return hashlib.sha256(f"{SALT}:{uid}".encode()).hexdigest()[:12]
def mask_phone(p: str) -> str: # 保留前三位与后四位,便于人工核对
return p[:3] + "****" + p[-4:]
3. 数据来源怎么选
| 来源 | 可复现性 | 真实度 | 适用 |
|---|---|---|---|
| 手写常量 | 高 | 低 | 单测里的关键值、错误路径 |
| 固定夹具文件 | 高 | 中 | 单测、接口测试的稳定输入 |
| 随机生成 + 固定种子 | 高 | 中 | 批量造数、分页与排序场景 |
| 生产快照脱敏 | 中 | 高 | 集成测试、性能测试、缺陷复现 |
一个经验值:单测全部用前三种,涉及真实数据分布与索引选择率的场景才用第四种,并且脱敏后的数据要入库到独立的测试环境,不能直接进 Git。
4. 脱敏:既要掩盖,也要还能复现缺陷
| 字段 | 处理方式 | 要点 |
|---|---|---|
| 手机号 | 保留前三后四,中间掩码 | 保持唯一性,否则唯一索引冲突 |
| 身份证 | 只保留出生年月,序号位随机 | 校验位需重算,避免录入校验失败 |
| 邮箱 | 保留域名,本地部分替换为哈希 | 同一用户必须得到同一结果 |
| 姓名 | 假名映射表,按 uid 稳定映射 | 重名率高会破坏「同名不同人」的用例 |
| 地址 | 只保留到城市一级 | 经纬度要一起改,否则地理查询结果异常 |
| 令牌、密码、验证码 | 直接删除,不要掩码 | 掩码仍是敏感串,容易误用 |
脱敏的验收标准是三条:关联字段仍然一致(order.UserID 仍能查到对应 order)、唯一约束仍然成立、分布仍然近似原始(别把 90% 的订单都变成同一个状态)。
5. 常见坑
- 共享夹具被改坏:builder 返回值拷贝、测试内不修改全局夹具变量;需要注入时在用例内构造。
- 时间依赖 now:夹具里出现
time.Now()后,跨天运行结果就不同。时钟应当作为参数注入,或用固定的基准时间。 - 依赖自增 ID:用例断言
id == 1时,只要前面多插一条数据就红。ID 应由夹具显式指定,或用「相对断言」(新记录的 ID 大于旧记录)。 - 夹具与表结构漂移:新增非空字段后夹具不更新,属于典型的「测试看起来在跑,其实插不进去」。让夹具构造器与模型定义放在同一个包,改动时编译器提醒你。
- 随机无种子引发偶发失败:CI 上「三跑两绿」的测试比没有测试更糟。所有随机源必须显式播种,并把 seed 打进日志。
- 脱敏破坏唯一约束:全量手机号掩码成同一个值时,批量插入直接失败。脱敏函数要带原值的哈希后缀。
- 用生产数据直接进仓库:即使是「测试数据」,只要来自生产就必须走脱敏流程并留记录。
小结:夹具用「默认值 + 显式覆盖」写法把用例的意图暴露出来,批量数据用固定种子的生成脚本保证可复现,来自生产的数据一律脱敏后再用;AI 负责把字段清单展开成构造器、生成器与脱敏函数,人负责确认默认值合法、唯一约束不破、以及脱敏后仍能复现目标缺陷。