用 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 负责把字段清单展开成构造器、生成器与脱敏函数,人负责确认默认值合法、唯一约束不破、以及脱敏后仍能复现目标缺陷。

笔记加载中…