菜单

可复现的测试数据:让自动化测试不再时好时坏

随机生成的测试数据会让流水线时好时坏。本文说明种子、算法与数据版本这三件事必须同时固定,怎么把一份样本长期钉住,以及哪些场景反而不该用固定样本。

发布于

  • 可复现
  • 自动化测试
  • 随机种子

随机测试数据的问题不在于「随机」,而在于不可复现。一个用例昨天通过、今天失败、明天又通过,而这几天里没人改过业务代码。这种失败最耗时间,因为你没法稳定地观察它,也没法把它交给别人去查。

随机数据在流水线里会造成什么

第一类问题是排查成本。流水线报错时你只拿到一条失败记录的内容,如果这条记录是运行时随机生成的,你无法把它塞回本地复现,除非当时把数据打了出来。很多流水线的日志只留断言信息,不留输入,于是只能靠重跑碰运气。

第二类是失败会传染。一个用例因为随机数据落到了边界组合而失败,重跑之后通过,于是团队习惯性地「重跑一次再说」。这个习惯一旦形成,真正的回归失败也会被当成抖动忽略掉,这才是随机数据最大的代价。

第三类更隐蔽:一批用例依赖同一份随机数据,某次运行中某个字段偶然超出了长度限制,同一个原因引发五条用例失败。你会先去查那五条用例之间的关系,方向从一开始就错了。

顺带说一句,随机会掩盖覆盖率。一个只跑一次的随机用例,无法说明某个分支是否被测过。

可复现需要哪三件事同时固定?

种子、算法、数据版本,缺一不可。

种子决定伪随机序列的起点。绝大多数语言里的随机数生成器都是伪随机:给定同一个种子,序列完全相同。所以固定种子是让生成结果稳定的最低成本手段。

算法指生成逻辑本身。库升级、权重表调整、字段顺序变化,都会让同一个种子产出不同结果。这就是为什么只固定种子不够,还要固定生成器的版本。

数据版本指底层的名称库、行政区列表、邮编规则。这部分最容易变,因为它常常来自外部数据,更新一次,同一条记录里的城市名就可能变。这类字段各自独立生成时会互相矛盾,成因见测试数据的字段自洽。

固定种子的具体做法

原则是种子不要写在函数内部临时生成,而是从外部传入并记录。用文字描述这段逻辑大致是这样:先从环境变量读取一个数据种子,缺省时回落到一个写死的值;用它构造随机数发生器;再用同一个发生器构造记录;最后断言记录里的字段等于用同一个种子算出来的期望值。

这段里真正有意义的地方不在取数的步骤,而在最后一步:期望值也来自种子,而不是硬编码一份「看起来对」的数据。如果期望值是手工填的,固定种子反而会让你以为问题解决了,实际上你只是把随机失败换成了固定失败。

第二件事是把失败时的输入落盘。流水线里加一步「失败则保存本次使用的种子和生成结果」,排查时直接拿回来本地重跑。这一步的成本很低,收益是每次抖动都能变成可复现的缺陷。

怎么把一份样本长期钉住?

固定种子仍然有一个限制:它绑定的是你这次用的生成器版本。更稳的做法是让生成过程无状态且可寻址,不用「下一个随机值是什么」,而用「给定这个标识,结果是什么」。

本站的身份信息生成器与地址生成工具就是按这个思路做的:所有工具共用一个身份标识,同一个标识加上同一个国家和性别,永远产出同一条记录。测试里需要稳定样本时,把标识写进固定装置,而不是把整条记录复制下来。复制记录的问题是它在数据版本更新后不会跟着变,而标识会。

批量导出时这一点尤其有用:同一批标识在本地和流水线里得到的结果一致,断言可以直接比对字段,不需要先跑一遍生成器把结果打出来看。需要换成一批新样本时,换标识即可,改动范围只有一行。这套思路在批量造账号时同样成立,见批量造测试账号。

哪些场景反而不该用固定样本?

可复现不等于必须恒定。有些测试要的正是多样性:想验证系统能处理任意合法输入时,固定一份样本反而没意义。这类测试的做法是把随机性限制在可控范围内,先随机生成,把当次使用的种子和样本落盘,失败时用这份记录复现,而不是每次都重新抽。

另一个边界是并发。多个测试并行跑时,如果共用一个全局种子,执行顺序会影响各自拿到的序列。要么每个测试有自己的种子命名空间,要么把生成阶段与测试执行分离,先生成固定数据再并行消费。

下表把两类场景对照一下,选择时不必纠结:

场景 该用固定样本吗 理由
单元测试断言 该用 期望值必须能被别人重建
接口回归 该用 输入稳定才能比对输出
截图对比 该用 渲染结果依赖输入
压力测试 不该用 需要多样性,只记录失败那一次
模糊测试 不该用 目的就是制造意外输入

常见问题

固定种子了,为什么流水线还是偶发失败?

先看失败的是不是数据相关的断言。常见原因有三个:种子只固定了一部分,比如用了系统时间做二次随机;测试并行导致共享状态被覆盖;断言依赖了集合遍历顺序。把数据生成和断言消费分开,通常能定位到具体是哪一个。

时间相关的数据该怎么处理?

不要让生成器直接读系统时钟。把「当前时间」作为一个显式参数传进去,测试里传固定值,运行时才传真实值。这一条对涉及证件有效期、年龄、订阅周期的字段尤其重要,否则用例会在某一天突然集体失败。

种子要不要提交到仓库?

要,而且要和失败的用例一起出现在日志里。种子本身不是秘密,它只是复现所需的输入之一。反过来,一个只存在于某次流水线日志里、事后无法找回的种子,等于没有。

可复现的样本能覆盖边界情况吗?

能,但要专门设计。固定种子只会稳定地复现你构造过的组合,不会自己长出新边界。可行的办法是在固定种子的基础上,把边界值作为一个显式参数暴露出来,让同一条样本生成逻辑既能产出常规记录,也能产出恰好等于长度上限、恰好差一个字符的记录。

本文讨论的是测试工程实践,不涉及任何具体产品、库或服务的性能与可靠性承诺;文中出现的字段、参数与流程均为通用描述,落地时请以你所使用的工具版本文档为准。

继续阅读

在线身份与测试数据生成器相关文章