一、先说那个所有人都遇到过的报错

「该手机号已注册」「设备名称已存在」「订单号重复」—— 写 E2E 的人对这类报错都很熟。它们通常不是因为脚本写错了,而是因为测试数据是写死的:

{ "action": "fill", "locator": { "strategy": "placeholder", "value": "手机号" }, "value": "13800138000" }

第一次跑没问题,第二次跑就撞车。跟着一起来的还有另外两个问题:把脚本从测试环境切到预发,一堆 http://localhost:8080 得手改;账号密码躺在脚本 JSON 里,谁都能看到。

这三个问题看着都像「测试数据问题」,其实需要两种不同的东西:

下面是我们这边落下来的规则和几个踩过的坑。

二、环境变量:挂在项目上,引用写 {{名字}}

环境变量挂在项目上(一个项目对应一个被测系统),入口在项目列表的行操作里:

项目列表:行操作里的 🔑 是环境变量入口

环境变量管理:键值对 + 引用说明

弹窗里那行提示就是全部契约:

脚本中用 {{变量名}} 引用,运行脚本时替换为对应值。变量名仅允许字母/中文/数字/下划线,且不以数字开头。

几个实现上的选择,都是被实际问题逼出来的:

替换发生在哪几处是固定的:instruction、url、value、locator.value、locator.name、locator.scope.value、locator.scope.name、assertion.expected、assertion.jsonPath。

最后两个最容易被忽略:断言里也该用变量。比如「新建设备后,详情页标题等于刚填的设备名」,就两处都写 {{设备名}},而不是把值抄一遍 —— 抄一遍,就等着它和随机数据对不上那天。

没定义的变量不会被静默吃掉:保留 {{xxx}} 字面量,同时在运行日志里告警:

[警告] 未定义的环境变量:测试账号、密码

这一点我们坚持了很久:把未定义变量替换成空字符串,会让脚本带着空值一路跑下去,然后在某个完全无关的步骤上报错,排查成本高得多。

用例详情:步骤表格里的 {{变量}} 占位符

三、系统变量:六种「每次都不一样」的数据

另一类是内置的、不需要定义的系统变量,写法一致:

变量 生成内容 例子
{{systemTime}} 当前时间戳(13 位毫秒) test_{{systemTime}} → 唯一账号
{{randomNumber}} 随机数字串,默认 6 位,可 {{randomNumber:8}} SN-{{randomNumber:8}}
{{randomChinese}} 随机常用汉字,默认 2 个,可 {{randomChinese:4}} user_{{randomChinese}}
{{randomPhone}} 随机手机号(1[3-9] 开头 11 位) 直接填手机号字段
{{randomEmail}} 随机邮箱 直接填邮箱字段
{{randomIdCard}} 随机 18 位身份证号(含校验位) 直接填证件号字段

「随机」这两个字在测试里是要讲质量的,几个细节:

用例列表:脚本里存的是占位符,运行时才替换

四、三条真正重要的规则

规则一:同一次运行内,同名变量值一致。

这是最容易做错的地方。系统变量不是「每次出现都随机一次」,而是运行开始时对用到的键求值一次、全程复用:

第 3 步 填写 手机号 → {{randomPhone}}      → 实例化 137****1234
第 9 步 断言 详情页手机号 = {{randomPhone}} → 同一个 137****1234 ✅

如果每处出现都重新随机,断言永远失败,而且失败得没有道理。

规则二:环境变量优先于系统变量。

同名时项目里显式定义的值覆盖内置变量。这是个「劫持开关」:某个环境必须用固定号码(走短信验证码的系统),把 randomPhone 定义成固定值就行,脚本一个字不改。

规则三:存模式,不存实例。

脚本里落库的永远是 {{randomPhone}} 这个占位符,不是它某次求值出来的号码。好处有两个:回放时重新求值 → 每次跑都是新数据,不会「第二次跑就撞已注册」;导出和 Code Review 时看到的是模式,不是一堆随机串。

五、一个真实的坑:fill 写了 A,断言引用了 B

上面三条规则都对,但生成场景里还有一层陷阱,我们是踩到真实案例才发现的:

模型在 fill 那一步没有照要求传占位符,而是自己编了一个值(test829401);等到后面断言里第一次出现占位符时才求值,得到 test442434。两边永远不会相遇 —— 而日志上每一步都是「成功」。

修法不是再嘱咐模型一遍,而是把它变成机制:fill / 组件动作成功后,把「实际写进页面的值」按模板反查,登记为该占位符键的实例化,之后断言和护栏都从这里取值。模型怎么发挥,都不影响一致性。

顺带把和「换值重试」的分工说清:缓存保证同一轮同值,显式换值会重新求值并覆盖缓存 —— 一个保一致性,一个提供不重复,两件事正交。

六、我们没做的几件事

七、想听听你们怎么管测试数据

我们这边收敛到「环境变量管共享值、系统变量管唯一值、运行时替换」这一套,但测试数据这块各家做法差别很大。我见过的还有:每个用例独立造数据的接口、测试库定时重置、按用例前缀隔离(case_1024_xxx)、以及直接用线上脱敏数据的快照。

你们团队现在怎么造数据、怎么避免残留数据把回归搞红? 欢迎在评论区聊聊,尤其是那些跑久了才会暴露的方式。

运行日志:未定义变量会明确告警

现状与限制

利益相关:我是 TestDog 的作者,本文写的是自己的实现与踩坑,不构成第三方评测。


↙↙↙阅读原文可查看相关链接,并与作者交流