尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Hypothesis 与属性测试的本质:从 QuickCheck 的遗产到 Conjecture 的实现

Hypothesis 与属性测试的本质:从 QuickCheck 的遗产到 Conjecture 的实现 测试开发工具【免费下载链接】hypothesisThe property-based testing library for Python项目地址https://gitcode.com/gh_mirrors/hy/hypothesis点击查看免费下载属性测试Property Based Testing是当前测试领域最值得掌握的方法论之一而 Python 生态中它的代表实现就是 Hypothesis。本文以 Hypothesis 作者对“什么是属性测试”的权威阐述为核心骨架系统梳理属性测试的边界哪些特性并非必要、fuzzing 与属性测试的分野、其“构造测试 让测试被 fuzz”的本质定义以及这一思想在 Hypothesis 仓库 中如何落地为真实的引擎实现——从结构化模糊测试核心 Conjecture到given装饰器、strategies策略与 shrinker最小化器的完整调用链。读完本文你将能用精确的语言界定属性测试理解它为什么不是“随机测试”的简单包装并能把 Hypothesis 的用法与其底层机制对应起来。从“QuickCheck 是什么”到“属性测试是什么”在很长一段时间里属性测试的工作定义就是“QuickCheck 所做的事情”。QuickCheck 诞生于 Haskell 生态quickcheck-in-every-language 一文 详细梳理了它在十余种语言中的移植情况这个定义用起来很方便但它有一个致命缺陷它无法区分属性测试的本质特征essential features与只是“在我们熟悉的实现中偶然出现的特征”accidental features。对 QuickCheck 的继承者们来说这个区别尤其重要——它们往往与 QuickCheck 在实现上差异巨大。Hypothesis 的作者明确指出属性测试并非由某个特定库定义而是由一类行为模式定义边界既可以划得很窄只收录长得像 QuickCheck 的东西也可以划得很宽收录同一行为家族的所有东西。本文采取宽边界但先钉下一根“旗帜”明确哪些东西不是属性测试的必要特征引用透明性Referential Transparency测试的代码不必是纯函数。类型Types强类型语言并非前提。随机化Randomization生成方式不一定要是随机的。使用任何特定工具或库手写测试协议同样可以构成属性测试。这四点分别有对应的反证几乎每个属性测试库Hypothesis、Erlang 与 Haskell 版 QuickCheck都允许有副作用的测试Erlang QuickCheck、test.check、Hypothesis 等大量动态语言实现都相当成功SmallCheck 按确定性穷举生成却毫无疑问属于属性测试至于“不用任何工具”——作者曾只用手写协议测试一个代码格式化器把一份 Python 文件语料跑过格式化器再检查输出是否满足 PEP8这就是一个经典的“带 oracle预言机的属性测试”。尤其值得展开的是第一点。针对“属性测试要求代码引用透明”的流行误解referential-transparency 一文 给出了更彻底的澄清这种观念来自早期 Haskell QuickCheck 的设计倾向更像形式化方法、且其宿主语言本身以纯函数为常态但就连最新版 Haskell QuickCheck 都完整支持在 IO 中测试属性。属性测试对副作用唯一的要求是如果测试有全局副作用它必须能在结束时回滚——而这恰恰与普通单元测试的要求完全相同。“属性测试只是普通测试多跑几次外加一个数据源来填掉一些空位而已。”这句话是理解整套方法论的心理起点。边界问题语料测试与 fuzzing 算不算属性测试只有正例无法得出好定义所以作者考察了两个有争议的边界案例。针对大语料回放大概率算如果取 SmallCheck 会生成的前 2 万个输出每次测试只回放其中前 N 个你做的其实是完全同类的测试用 Hypothesis 抽出 2 万个输出后再随机抽样亦然。因此针对大语料测试“大概算”属性测试。反例是小且固定的语料如果你能把它直接写成源码里 10 条 example-based 测试那它实质上就是示例测试。这条边界确实有点模糊。针对 fuzzing作者收回了此前的观点作者曾主张“fuzzing 只是属性测试的一种形式——你在测试‘它不会崩溃’这一属性”但本文正式反转了这一观点他倡导的入门方式见 getting-started-with-hypothesis大概率不算属性测试。理由是两者的气质不同属性测试要求你思考程序应当如何表现而 fuzzing 几乎不需要理解程序行为就能套用到任意代码上且 fuzzing 感觉上更底层、更基础。但两个方向都可以跨越你可以用 fuzzing 工具做属性测试例如给上面的格式化器测试加上 python-afl你也可以用属性测试工具做 fuzzing——所以“并非所有用 Hypothesis、QuickCheck 写的测试都是属性测试”作者对此坦然接受认为这符合测试工具被跨界使用的长久传统。由此给出作者希望采用的 fuzzing 定义Fuzzing 是向一段代码函数、程序等喂入来自大语料的数据——可能是动态生成的也可能依赖于对先前数据的执行结果——以观察它是否会失败。“数据”和“是否失败”的定义随 fuzzer 而异有的只生成二进制数据有的生成更结构化的数据有的寻找进程崩溃有的只寻找函数返回 false。作者还特别指出常见的“fuzzing 专指畸形数据”的界定是有问题的——CSmith 显然是一种 fuzzer但它刻意只生成结构良好的 C 程序。属性测试的正式定义与两段式本质在 fuzzing 定义的基础上文章给出了核心定义属性测试是这样一类测试的构造当这些测试被 fuzz 时测试中的失败能够揭示被测系统system under test中那些直接 fuzzing 该系统所无法揭示的问题。如果你坚持认为 fuzzing 本身就该算属性测试只需去掉“无法被直接 fuzzing 揭示”这一从句即可——作者本人也在这条界线上摇摆。这些额外暴露出的失败模式就是我们在测试的“属性”。这个定义有一个作者特别珍视的推论属性测试是“你”做的事不是计算机做的事——计算机那部分“只是 fuzzing”。由此一个属性测试库天然地由两部分组成一个 fuzzer负责生成输入、寻找失败一组让“用这个 fuzzer 构造属性测试”变得容易的工具策略库、装饰器、最小化、数据库等。Hypothesis 正是严格按这条思路设计的它的核心是一个名为Conjecture的结构化 fuzzing 库。回到仓库Conjecture 如何实现“结构化 fuzzing”“Hypothesis 的核心是名为 Conjecture 的结构化 fuzzing 库”这句话在当今仓库里可以被精确地验证。Conjecture 的源码位于 hypothesis/src/hypothesis/internal/conjecture/入口在init.py引擎在 engine.py。引擎的核心类是ConjectureRunnerengine.py 的 class ConjectureRunner它围绕test_function(data: ConjectureData)运行每次测试把一段字节缓冲解释为一串“选择”choices由ConjectureData提供给被测代码。从源码结构可以清晰地看出它的 fuzzer 属性engine.py顶部定义了BUFFER_SIZE 8 * 1024单个测试用例生成阶段可消耗的最大熵预算、MIN_TEST_CALLS 10、MAX_SHRINKS 500、MAX_SHRINKING_SECONDS 300等常量勾勒出“生成—测试—收缩”的运行框架。与普通 fuzzer 不同Conjecture 的生成是结构化的策略strategies不直接吐字节而是通过draw_choice等原语按需从数据缓冲区中抽取选择见 choice.py 中的ChoiceNode、choice_from_index、choice_to_index等机制。这解释了为什么 Hypothesis 能生成嵌套字典、合法时间、递归结构而不只是字节流——这正是“结构化 fuzzing”的含义。收缩shrinking让失败样例“最小化”属性测试工具区别于裸 fuzzer 的关键体验之一是失败样例的最小化。Conjecture 的收缩器实现在 shrinker.py其工作方式是把一次失败的执行重放为一棵选择树ChoiceTree然后不断尝试“更简单”的选择序列——更短或同样长度但对应索引更小sort_key的定义见 shrinker.py 开头只要新序列仍然导致同样的失败interesting_origin相同就保留。引擎层通过shrink_interesting_test_casesengine.py 中的方法把所有已发现的失败样例逐个替换为最小复现。结合 encode/decode 一文 的实例可以直观感受到这一点测试decode(encode(s)) s时Hypothesis 给出的最短失败样例是s110两个相同字符后跟一个不同字符——它“不是”作者偏好顺序下的绝对最小样例那会是001但已经足够简单、可读足以快速定位“没有重置计数”的缺陷。最小化不是可有可无的装饰它直接决定你调试体验的好坏。为什么失败可能“超出被直接 fuzz 能揭示的问题”回到定义中的关键从句属性测试的失败能揭示直接 fuzzing 无法揭示的问题。用仓库里的真实例子说明纯 fuzzing 发现的问题RLE 的encode处理空字符串时抛UnboundLocalError——这是“不崩溃”属性的失败直接 fuzzing 也能发现getting-started-with-hypothesis 中作者承认这种入门式测试“大概率不算属性测试”。属性本身发现的问题decode(encode(s)) s在s110上失败——这个失败只有在“编码再解码等于原样”这一属性被显式断言时才可能暴露直接给encode或decode喂随机数据是抓不到的。这正是“测试被 fuzz 时失败揭示了直接 fuzzing 无法揭示的问题”的活例子属性是额外的一层失败模式探测器。实操层用 Hypothesis 写出“属性测试”理解了定义再看 Hypothesis 面向用户的 API——它对应定义中的第二组成部分“让构造属性测试变容易的工具”。given装饰器与策略strategies属性测试的入口是given参数是策略。策略的公开入口在 hypothesis/src/hypothesis/strategies/init.py它从_internal子模块导出integers、text、lists、sampled_from、one_of、from_type、recursive、composite等数十种构造器整数与浮点定义在_internal/numbers.py字符串在_internal/strings.py组合器one_of在_internal/strategies.py。一份最小可用示例from hypothesis import given, reject from hypothesis.strategies import integers, text given(integers(), text()) def test_some_stuff(x, y): try: my_function(x, y) except SomeExpectedException: reject()Hypothesis 会用这两个策略反复调用测试函数直到发现一个产生意外异常的组合当遇到“已知可能”的异常例如参数越界导致的ValueError时调用reject()丢弃该样例——被丢弃的样例不会计入允许运行的示例预算这种“只测不崩溃”的写法在定义上是 fuzzing 风格作者明确说它是进入属性测试的良好起点但还不是完整的属性测试。从 fuzzing 走向属性测试的两条路getting-started-with-hypothesis 给出了从“入门 fuzzing”升级到“真正属性测试”的两条路径恰好呼应本文的定义断言函数结果的任何性质返回类型是什么能否为None与输入存在什么可检验的关系——哪怕是非常琐碎的属性也有价值让代码更防御性把代码里错误的假设变成崩溃而非静默的状态损坏——例如为函数增加参数检查Hypothesis 为此设有专门的InvalidArgument异常或在代码里大量添加断言把“局部性质”编码为断言。第 2 条是作者认为产出最高的路径它让代码即使没被测出 bug 也变得更健壮同时让你一次只关注问题的一半平滑进入属性测试。属性测试的经典范式Encode/Decode 不变式一旦上手最容易找到的属性之一就是编码/解码对存在一个把值编码为另一表示的函数和另一个应当逆转该过程的函数。这类不变式天然有完全明确的规格——“编码再解码应该等于什么都没做”——非常适合 Hypothesis因为序列化到表单、API、数据库的场景无处不在。仓库里的完整示例encode-decode-invariantfrom hypothesis import given from hypothesis.strategies import text given(text()) def test_decode_inverts_encode(s): assert decode(encode(s)) s这个看似“平凡”的不变式先通过纯 fuzzing 抓到了空字符串的UnboundLocalError修复if not input_string: return []随后当实现里被人为删除“字符变化时重置计数”的一行后测试在s110上失败assert 1100 110。不变式测试的妙处正在于此它把纯 fuzzing 也顺带包含进来了——即使平凡的不变式也常常能发现有趣的问题。属性测试库的职责划分对照表综合文档与源码可以把“属性测试库 fuzzer 测试构造工具”这个两分法映射到 Hypothesis 的对应实现定义中的组成部分Hypothesis 中的对应物仓库位置Fuzzer生成输入、寻找失败Conjecture 引擎ConjectureRunnerhypothesis/src/hypothesis/internal/conjecture/engine.py结构化数据生成Choice / ConjectureData 抽取机制hypothesis/src/hypothesis/internal/conjecture/choice.py失败样例最小化ShrinkerChoiceTree 遍历hypothesis/src/hypothesis/internal/conjecture/shrinker.py构造属性测试的工具given、strategies、composite、from_type等hypothesis/src/hypothesis/strategies/init.py属性测试的书写范式encode/decode、往返roundtrip、不崩溃等不变式website/content/2016-04-16-encode-decode-invariant.md结语一种可以迁移到任何语言的方法论回到开头的问题——“什么是属性测试”——本文给出的最终答案不是“QuickCheck 做的事”而是一句可操作的判断标准当你的测试被 fuzz 时它所暴露的失败模式是否超出了直接 fuzzing 被测系统所能暴露的范围若是你就在做属性测试。而属性测试之所以是“你”做的事情是因为计算机只是负责 fuzzing 的那一半另一半——想清楚系统应该有什么行为、把这些行为写成可断言的属性——永远是写测试的人的工作。Hypothesis 的全部价值就是让这后一半工作变得足够便宜策略负责构造数据Conjecture 负责寻找并收缩失败而你只需要写出那条不变式。这套思想完全不绑定 Python它同样适用于 quickcheck-in-every-language 中列出的任何语言与工具Hypothesis 只是把这条思路贯彻得最彻底的那个实现之一。赞分享测试开发工具【免费下载链接】hypothesisThe property-based testing library for Python项目地址https://gitcode.com/gh_mirrors/hy/hypothesis点击查看免费下载相关推荐Hypothesis 对计算机科学研究者的价值从 QuickCheck 到 Conjecture 引擎的探索Hypothesis 对计算机科学研究者的价值从 QuickCheck 到 Conjecture 引擎的探索 Hypothesis 是 Python 生态中广测试开发工具10分钟给 WeMod 装上本地增强版Wand-Enhancer 完整上手指南10分钟给 WeMod 装上本地增强版Wand Enhancer 完整上手指南 还在为 WandWeMod客户端功能太少、没法拿手机远程操控而头疼Wan测试开发工具公式图片怎么快速转成 LaTeX 代码LaTeX-OCR 4 种用法实战指南公式图片怎么快速转成 LaTeX 代码LaTeX OCR 4 种用法实战指南 写论文时参考文献里 50 个公式要手敲进 LaTeX敲到第 12 个手已经开测试开发工具上一篇Shizuku开发者指南集成与API使用最佳实践下一篇从源码到桌面应用GitHub Desktop全平台构建与部署指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表