42 基于特性测试

用生成器探索输入空间,用不变量裁决结果,并通过缩减找到最小反例。

学习目标

  • 能把业务假设写成生成器、前置约束、不变量和拒绝条件
  • 能固定随机种子运行特性测试,在失败后缩减出可读、可回归的最小反例
  • 能区分生成器缺陷、性质缺陷和实现缺陷,并以重放证据选择修复路径

让测试探索假设,而不是堆例子

本页依据 David Thomas、Andrew Hunt《程序员修炼之道:通向务实的最高境界(第2版)》,云风译,电子工业出版社,2020 年 4 月,ISBN 9787121384356 的公开中文目录,独立重构 42 基于特性测试。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图或答案。

手工样例擅长表达几个重要故事,特性测试则用生成器探索一组输入,用不变量裁决输出是否仍符合假设。它不是“随机多跑几次”,而是把输入空间、合法边界、结果关系、随机种子和失败缩减都写成可复核协议。生成器越强,性质越准确,反例才越有信息量。

三个会让特性测试失真的陷阱

从生成器到最小反例

<Term def="根据约束构造一组可复现输入,并覆盖边界、组合和分布变化的输入程序。">生成器</Term>决定测试探索什么。生成器不是越随机越好;它应说明合法域、边界权重、组合规则和种子。一个金额生成器若从不产生零、极大值或重复商品,就无法验证对应风险。

<Term def="规定输入是否合法、性质何时适用以及非法输入应如何被拒绝的前置规则。">前置约束</Term>把探索空间与业务规则连接起来。有效购物车可以要求数量非负、货币一致;域外样本则应验证拒绝原因,而不是强行代入总价守恒性质。

<Term def="对输入和输出必须持续成立的业务关系,如守恒、幂等、可交换、往返或单调关系。">不变量</Term>要能被反例推翻。对购物车可以写:合并空购物车不改变结果;同一幂等键重复结算不增加订单;按项拆分再合并的总价相等。每个性质都要说明舍入、时区和精度边界。

基于特性测试:探索、裁决,再解释失败种子保证重放,缩减把随机失败变成可读反例1生成器探索输入空间已留证据2前置约束分开合法域已留证据3不变量定义业务关系已留证据4随机运行记录种子重放已留证据5缩减留下最小反例当前解释入口失败后先分类:生成器、性质还是实现;再决定如何修复
专属图示:特性测试把输入域和业务关系连接成可重放的失败链。

42 与提示 71:使用特性测试来校验假设

本单元对应中文目录的 42 基于特性测试提示 71:使用基于特性的测试来校验假设。提示的实践含义是:当你相信某个关系对一类输入成立时,用生成器扩大探索,用不变量定义裁决,并让缩减结果帮助你修正假设。

type Cart = ReadonlyArray<{ sku: string; quantity: number; price: number }>;
 
function total(cart: Cart): number {
  return cart.reduce((sum, item) => sum + item.quantity * item.price, 0);
}
 
function addEmpty(cart: Cart): Cart {
  return [...cart];
}

可以验证 total(addEmpty(cart)) === total(cart),也可以生成两组合法购物车,检查合并后总价等于分别计算之和。若金额有小数,就要先声明精度与舍入规则;否则失败可能来自性质表述,而不是实现。

缩减:让失败变得可理解

<Term def="在保持性质失败的前提下,逐步移除元素、缩小数值或简化结构,得到最小可读失败输入的过程。">缩减</Term>是特性测试的解释阶段。一个包含五十件商品、三种货币和多个时区的失败样本很难定位;如果能缩成两件商品和一个重复键,团队才能讨论真正的规则。

缩减器也需要审查。若它删除了触发故障的关系,最后的样本可能变成绿色,测试工具却报告“没有最小反例”。保存原始输入、每一步缩减和最终失败输入;若缩减失败,保留未缩减样本并明确限制,不能假装已经定位。

种子、版本与重放

<Term def="决定生成器选择顺序的可记录值;结合生成器版本和运行环境可以重建一次探索。">随机种子</Term>是重放协议的一部分。记录种子还不够:生成器代码、库版本、平台、时区、前置约束和执行次数也可能改变序列。回归测试应直接加入缩减后的最小反例,种子保留为调查来源。

一次通过也有边界。若只运行固定种子,探索会变窄;若每次使用不可记录的新种子,失败又无法重放。推荐保存一组稳定种子、定期轮换的探索种子,以及每次失败的完整生成信息。

练习实验:从性质到回归

Interactive lab

选择样本,判断失败属于哪一层

边界拒绝

输入证据

负数量与混合货币被生成器标出,接口返回明确拒绝。

实际首差

前置节点:域外输入不进入业务性质。

恢复动作

保存约束、边界值和拒绝原因。

先写性质和拒绝规则,再选择样本;最小反例比“运行了很多次”更有复核价值。

分步1 / 4

1. 选择变换与业务关系

写出一个有意义的变换,例如加入空项、合并购物车、序列化后再解析。为它定义至少两个不变量,并明确精度、非法输入和拒绝条件。

正常、边界与单一故障证据

证据矩阵:性质、种子和最小反例缺一不可有效域、拒绝域和失败域必须分别说明如何复核字段正常边界故障输入合法多样域外样本生成偏斜性质关系成立明确拒绝关系失败首差前置节点不变量恢复保存种子记录原因缩减回归保存原始样本、缩减过程、最小反例、种子和生成器版本,才能重放
专属图示:正常、边界和故障样本揭示性质适用的真实范围。
样本唯一变化预期判定必存证据
正常合法生成器与稳定性质多样输入均满足关系种子、样本摘要和性质结果
边界空、零、极大或域外输入接受或拒绝规则明确前置约束、边界值和原因
单一故障一个实现、性质或生成器条件失效首个失败关系暴露原始样本、缩减过程和回退

独立复核者应能用种子和版本重建失败,再确认最小反例仍然失败。测试通过次数不能替代性质质量;如果生成器只产生容易的输入,绿色结果的范围必须明确写出。

区分三种失败

生成器失败表现为样本不满足前置约束、分布偏斜或无法缩减;性质失败表现为关系写错、舍入规则遗漏或前提过宽;实现失败表现为合法输入确实违反一个清晰不变量。先分类再改代码,避免把错误性质“修复”成更弱的断言。

工具可以自动生成和缩减,但团队仍要评审它覆盖的输入域、忽略的副作用、随机策略和错误报告。涉及时间、并发、网络或权限时,给生成器注入可控时钟和资源边界;否则随机样本会把环境噪声伪装成业务反例。

术语表

名词解释

本章出现的专业名词,用大白话再讲一遍。

生成器

根据约束构造并探索一组可复现输入的程序。

前置约束

规定输入是否合法、性质何时适用以及域外输入如何拒绝的规则。

不变量

对输入和输出必须成立、可以被反例推翻的业务关系。

缩减

在保持失败的前提下简化样本,得到最小可读反例的过程。

随机种子

决定生成顺序、可与生成器版本一起重建探索的记录值。

练习

练习

问题 1: 你要测试“合并购物车后总价等于分别计算再相加”。需要哪些前置约束?

问题 2: 特性测试找到一个包含 100 条记录的失败样本,缩减后变成一条记录但仍失败。如何判断缩减是否有效?

问题 3: 固定种子连续运行 10,000 次都通过,但生产出现失败。你会如何更新证据?

资料与写作方式声明

本章以程序员修炼之道权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

本单元回顾

基于特性测试,不是把随机性当成证明,而是把输入域、前置约束、不变量、种子和缩减都变成可复核证据。好的失败会变小、能重放、能进入回归;好的通过则清楚说明生成器探索了什么、性质没有覆盖什么,以及哪些环境条件仍需额外测试。

前后导航

讨论

评论区加载中…