- 测试
- 开发工具
【免费下载链接】hypothesis
The property-based testing library for Python
Hypothesis 是 Python 生态中广泛使用的 property-based testing(属性测试)库。本文源自项目作者 David MacIver 于 2017 年撰写的技术长文(见 website/content/2017-03-09-hypothesis-for-researchers.md),面向潜在博士导师与测试/验证领域研究者,系统回答了四个问题:Hypothesis 是什么、创新点在哪里、基于哪些先验工作、以及有哪些值得投入的研究方向。文中所有示例均可直接在本仓库中运行验证,源码引用以 hypothesis/src/hypothesis/internal/conjecture/ 下的实现为准,可将其视为"可运行的研究文献"。
研究视角下为什么要关注 Hypothesis
简短回答:Hypothesis 把一种已在实践中被证明高效的测试风格——property-based testing——带给了更广泛的受众。它通过组合此前在测试与验证研究文献中彼此孤立的多条思想线索,产生了一个在实践中被验证为非常有效的全新实现。
详细回答就是本文的其余部分。文章按以下脉络展开:
- What is Hypothesis?:从零介绍 Hypothesis,如果你已熟悉 QuickCheck 这类 property-based testing 可跳过;
- How is Hypothesis innovative?:介绍 Hypothesis 的当前技术水准及其有趣之处;
- What prior art is it based on?:给出支撑 Hypothesis 设计的主要文献脉络,篇幅短但值得一读;
- What are some interesting research directions?:探索作者对 Hypothesis 未来走向的思考,其中部分有望进入相关博士课题。
什么是 Hypothesis:属性测试的现代实现
Hypothesis 是property-based testing的一个实现。这一思想起源于 Haskell 库 QuickCheck:用结构化随机数据补充单元测试,让工具自动探索测试的边缘情况、尝试发现错误。属性测试的核心是"描述性质而非手写用例"——你只需声明一段代码应当始终满足的性质,测试框架负责生成尽可能多的输入去验证它。
一个可运行的例子:排序的幂等性
下面的测试断言"对同一个列表排序两次结果不变":
from hypothesis import given, strategies as st @given(st.lists(st.integers())) def test_sort_is_idempotent(ls): sort1 = sorted(ls) assert sorted(sort1) == sort1@given装饰器把普通函数暴露给标准测试运行器(如 pytest);也可以直接调用:
if __name__ == "__main__": test_sort_is_idempotent()运行时,Hypothesis 会生成随机整数列表并传入测试函数:先排序一次,再排一次,断言两次结果相等。只要每个输入都通过,它看起来就是个普通测试。
关键差异发生在失败时:Hypothesis 会反复用"逐渐更简单"的例子重跑测试,直到找到一个引发失败的最小输入。假如我们写了一个错误的排序实现:
def sorted(ls): return list(reversed(ls))运行后输出:
@given(st.lists(st.integers())) def test_sort_is_idempotent(ls): sort1 = sorted(ls) > assert sorted(sort1) == sort1 E assert [0, 1] == [1, 0] E At index 0 diff: 0 != 1 E Use -v to get the full diff sorting.py:12: AssertionError ---- Hypothesis ---- Failing test case: test_sort_is_idempotent(ls=[0, 1])Hypothesis 最初很可能从更复杂的例子开始(几乎所有长度大于 1 的列表都会失败),然后成功约简为最简情形:一个含两个不同元素的列表。这就是"shrinking(最小化)"机制:在 engine.py 的shrink_interesting_test_cases中,引擎把每个失败用例替换为具有相同interesting_origin的最小失败用例,并对过程中发现的新失败持续收缩,直至 500 次收缩上限(MAX_SHRINKS,见 engine.py)或 300 秒总时长上限(MAX_SHRINKING_SECONDS,见 engine.py)被触发。
更重要的是:测试重跑时,Hypothesis 会从上次找到的失败例子出发,而不是重新生成并收缩一个新例子(见reuse_existing_test_cases,engine.py)。对简单用例这差别不大,但对复杂、慢速的测试而言,这是开发工作流的关键一环:测试跑得更快,并且在 bug 真正修复前不会停止失败。这也是"示例可保存、可回放"能力的底层支撑——数据库模块(database.py)负责把失败用例序列化为字节并持久化,重跑时按最短优先策略取回。
测试中动态抽取数据
Hypothesis 允许测试在执行过程中按需继续抽取数据:
@given(st.lists(st.integers(), min_size=1), st.data()) def test_sort_is_idempotent(ls, data): ls.sort() i = data.draw(st.integers(0, len(ls) - 1)) assert ls[i - 1] <= ls[i]这个测试失败,因为忘了i可能为 0,也忘了 Python 列表的负索引:
@given(st.lists(st.integers(), min_size=1), st.data()) def test_sort_is_idempotent(ls, data): ls.sort() i = data.draw(st.integers(0, len(ls) - 1)) > assert ls[i - 1] <= ls[i] E assert 1 <= 0 sorting.py:15: AssertionError ---- Hypothesis ---- Failing test case: test_sort_is_idempotent(ls=[0, 1], data=data(...)) Draw 1: 0以这种方式抽取的数据,其收缩与失败示例保存同样正常工作。这个"互动式抽取"能力正是 Hypothesis 区别于 QuickCheck 传统模型的核心:data.draw在运行时向引擎请求更多字节流(st.data()策略定义于 core.py),引擎的ConjectureData.draw(data.py)负责把字节流翻译回策略值。
模型化测试(Model-Based Testing)
Hypothesis 还提供规则化状态机测试:你定义一组作用于 API 的合法操作,它尝试用这些操作拼出完整"程序"并寻找能破坏不变量/断言的简单序列。实现位于 stateful.py:RuleBasedStateMachine类(stateful.py)配合rule(stateful.py)与precondition(stateful.py)装饰器,用Bundle在规则之间传递产生/消费的值。这种测试形态把 property-based testing 从"单次输入的性质"推广到"整个交互序列的性质"。
Hypothesis 的创新之处
从最终用户视角,Hypothesis 带来了几项重要改进:
- 它真实存在且有人用:这类测试历史上主要活跃于函数式编程社群,在其他语言中的推广鲜有成功。Hypothesis 能做到,一部分归功于其新颖的实现细节,一部分归功于让它"感觉像普通测试而非形式化方法"的设计决策。
- 生成器定义更简单,且"免费"获得大量功能:与传统 QuickCheck 风格相比,Hypothesis 的 API 设计提供了显著更高的灵活性——这一点与 Clojure 的 test.check 或 Erlang 版 QuickCheck 相似,但若干设计决策使其更灵活。
- 任意示例可保存与回放:这大幅改善了开发工作流。其他 property-based testing 实现要么完全不保存、只保存随机种子,要么依赖序列化生成对象(读回时可能破坏不变量)。Hypothesis 的引擎天然支持字节级序列化,不需要策略层参与。
- 测试内可动态生成额外数据:这一能力似乎在同类别工具中是 Hypothesis 独有的,其底层就是上一节的
data.draw机制。
这些特性共同作用,相当有效地把 property-based testing 带给了"大众",Hypothesis 在 Python 社群中的使用越来越广泛,并被用于工具与库的开发中,包括 CPython 和 PyPy 这两大 Python 实现自身的开发。
实现层面的核心创新
上述用户侧优势大多源于实现上的根本差异:与其他 property-based testing 实现不同,Hypothesis 完全不需要理解它正在生成的数据结构(它偶尔会猜测结构,但正确性不依赖这些猜测的准确性)。
Hypothesis 在逻辑上分为三个相互独立的部件:
- 核心引擎 Conjecture:可视为一个面向"轻结构化字节流"的交互式 fuzzer。它负责生成、收缩、序列化——策略实现无需感知这些特性即可正确工作,只需反复向引擎索要字节块并返回期望的结果。
- 策略库(strategy library):把 Conjecture 的输出翻译为语言中可表示的各种值。例如
st.lists(elements, min_size=0, max_size=None, unique=False, unique_by=None)(core.py)支持长度区间、元素唯一性等约束,st.integers(min_value, max_value)(numbers.py)生成的整数向 0 收缩。 - 外部测试运行器接口:接收建立在策略库之上的测试并用 Conjecture 执行它们。在 Python 中这主要是暴露一个可被运行器识别的函数(
@given装饰器),而在 Java 原型中则涉及与 JUnit 特定特性的交互。
Conjecture 正是 Hypothesis 实现中最有趣的部分,支撑了绝大部分功能。在源码中,ConjectureRunner(engine.py)维护随机源、数据库键、感兴趣失败用例集与帕累托前沿等状态;其run()(engine.py)按reuse(复用数据库用例)→ generate(生成新用例)→ shrink(收缩失败用例)三阶段推进(见_run,engine.py),与Phase配置(reuse/generate/target/shrink/explain)一一对应。ConjectureData(data.py)则以max_choices、prefix、observer等字段记录单次测试运行的所有抽取与状态。
基于哪些先验工作:文献脉络
在开发 Hypothesis 的过程中,作者进行了大量文献阅读。支撑其设计的两篇核心论文是:
- QuickCheck: a lightweight tool for random testing of Haskell programs:基本开创了整个 property-based testing 领域。Hypothesis 最初就是一个 QuickCheck 实现,其面向用户的 API 至今仍深受 QuickCheck 影响——尽管底层实现已与其分道扬镳(上文的
@given+ 策略组合风格即是明证)。 - EXPLODE: a lightweight, general system for finding serious storage system errors:提供了 Conjecture 引擎的关键思想——不做与测试分离的静态数据生成,而是给测试提供一个可从中抽取数据的交互式原语(对应本仓库中的
data.draw与ConjectureData.draw)。
此外,Conjecture 引擎还有两个重要的设计灵感来源,虽然其设计当前未被直接使用:
- American Fuzzy Lop(AFL):优秀的面向安全的 fuzzer。作者从中学习了不少 fuzzer 设计知识;由于多种务实原因,目前没有使用它最重要的创新(用分支覆盖度量驱动语料发现),但已在 Hypothesis 之上成功原型化了该实现且效果不错。
- Swarm Testing:推动了早期数据生成设计的许多决策。当前并未显式出现在 Conjecture 实现中,但 Conjecture 为诱导数据中"刻意关联"所做的一些工作受其启发。
有趣的研究方向
作者列出了若干可能的研究方向。需要说明:这些方向并不必然成为博士课题的焦点——真正做博士时几乎肯定会聚焦更具体的研究问题。值得注意的共同点是:大多数方向都能在不改变 Hypothesis 公开接口的前提下带来改进,这意味着由于大量(且不断增长的)开源项目已在用 Hypothesis,许多改动可以仅通过"跑别人现成的测试、看能否发现新 bugs"来部分验证——这是极具实操优势的研究环境。
更结构化的字节流
当前最直接的研究焦点:把 Conjecture 的核心原语替换为更有结构的形式,使其更贴近 EXPLODE 的起源。这旨在解决用户当前遇到的实际问题(大多与性能相关),同时为构建在核心引擎之上的新抽象打开空间。
具体设想是精简接口:调用 Conjecture 时只抽取单个字节,并指定合法字节的取值范围。这让引擎获得更细粒度的信息,从而支撑更多新特性与抽象。由此原语可重建出"能正确收缩的任意加权采样器"(使用 Alias Method 的变体)与任意文法(可能使用 Boltzmann Samplers 或类似方法)。这比当前"有点临时的字节流指定方式"为高质量数据生成提供了更扎实的基础。这或许更多是工程而非研究,但至少能让关于核心方法的论文更有说服力,且包含大量有趣的理论应用。
玻璃箱测试(Glass Box Testing)
当前 Conjecture 把测试视为黑箱,几乎拿不到"测试在执行什么"的信息。
一个明显的方向是借鉴 AFL 的思路引入更多覆盖信息,但作者坦承:目前这些原型方案在真实场景中尚不理想——主要原因在于,所有已尝试的技术在"允许测试运行数分钟或数小时"时表现良好,而 Hypothesis 当前的设计目标是测试最多运行数秒,这限制了这些方法的效用,因此尚未成为优先级。
但原则上这应是一条极富成效的路线。主要设想是给 Conjecture 核心引擎加入"tags"概念用于引导搜索:覆盖信息只是 tags 的一个来源,其他来源同样可能。例如作者之前关于 Schroedinteger 的工作实现了某种轻量级 Concolic testing,可成为另一个有趣的信息来源。如何在严格受限的时间内用好这类信息,很可能结出有趣的果实;观察 Concolic testing 在真实世界的表现也会引出大量新问题。
让 Conjecture 引擎更聪明
作者过去曾研究用文法推断(grammar inference)改进收缩与数据生成。当时遇到的障碍是所用算法——L* 搜索的优化变体——在实际问题上性能不佳。
"Synthesizing Program Input Grammars" 一文承诺通过在实际场景中提供更好的文法推断来解除这一限制,而该场景与这一问题域密切相关,因此值得重新审视。此外,结合玻璃箱测试特性,Conjecture 引擎很可能还有多种探测被测系统状态、发现潜在有趣行为的方式。由于此前连"可接受的性能"都未达到,第一步自然是验证能否达到;这还需要大量实证实验,此时"用 Hypothesis 测试的开源项目语料"将极其有用。
其他测试抽象
尽管 Hypothesis 首先是 property-based testing 库,核心 Conjecture 引擎本身其实与属性测试关系不大,而是一种更强大的底层测试抽象。探索它能走多远会很有趣——现有的状态机/模型测试已是迈向该方向的一步,但引擎还可更直接地用于其他目的,例如配合上述特性对二进制进行底层 fuzzing,或驱动线程调度。
Conjecture 分离设计的好处在于:它足够自包含,可被当作核心构建块,让其他工具在其上重建并获得大量主要特性。作者目前没有具体计划,但认为在进一步研读测试文献后,这里很可能浮现出有趣的可能性——即便目前看来多半是工程工作,除非出现特别有趣的应用。
你应该如何消化这些信息
取决于你是谁:
- 如果你已经是潜在博士导师,请告诉作者什么引起了你的兴趣,并多提问;
- 如果你是尚未接触的潜在博士导师且有意向,欢迎直接联系;
- 如果你是其他读者,主动权在你——可以发送论文、问题等任何内容。
无论你是谁,若觉得这篇文章有意思,都可以通过 david@drmaciver.com 与作者联系。
延伸阅读与验证路径
若要亲自验证本文论述,最直接的路径是:
- 在仓库中运行 hypothesis/tests/ 下的测试集,特别是 hypothesis/tests/cover/(覆盖核心功能)与 hypothesis/tests/conjecture/(针对引擎本身的测试,如
test_engine.py、test_shrinker.py、test_provider.py); - 阅读 guides/internals.rst 与 hypothesis/docs/reference/internals.rst 了解引擎内部设计;
- 参考 hypothesis/rust/ 下用 Rust 实现的部分底层原语(如
cathetus与浮点处理),体现引擎跨语言复用的方向; - 若想从更高层面理解设计取舍,可阅读 website/content/2016-12-10-how-hypothesis-works.md。
从 2017 年至今,Conjecture 的三阶段运行框架、失败用例回放、动态抽取等核心思想在 hypothesis/src/hypothesis/ 中依然清晰可辨——这正是本文所描述的架构持续演进、并被验证有效的直接证据。
- 测试
- 开发工具
【免费下载链接】hypothesis
The property-based testing library for Python
相关推荐
BaiduPCS-Go学术论文:在计算机科学领域的研究价值
BaiduPCS Go学术论文:在计算机科学领域的研究价值 摘要 BaiduPCS Go作为一款开源的百度网盘命令行客户端,在计算机科学领域具有多方面的研究价值
CLI网络OpenCore Legacy Patcher:三步让老旧Mac焕发新生,体验最新macOS的终极指南
OpenCore Legacy Patcher:三步让老旧Mac焕发新生,体验最新macOS的终极指南 你是否有一台被苹果官方"抛弃"的老旧Mac?看着手中的2
测试开发工具torchao模型优化的跨学科研究:从计算机科学到神经科学
torchao模型优化的跨学科研究:从计算机科学到神经科学 在人工智能模型日益复杂的今天,如何在有限的计算资源下实现高效训练与推理成为关键挑战。torchao作
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考