- 测试
- 开发工具
【免费下载链接】hypothesis
The property-based testing library for Python
导读
本文基于 Hypothesis 官方文档中的 How-to 指南(hypothesis/docs/how-to/index.rst),系统梳理五个高频实战场景:全局抑制健康检查、为策略编写类型提示、实现自定义 ExampleDatabase、动态检测 Hypothesis 测试,以及将 Hypothesis 与 Atheris 等外部模糊测试器结合。每部分均附带完整可运行的代码示例,并结合仓库源码(_settings.py、database.py、core.py等)补充底层实现细节,帮助读者在真实项目中直接落地。
一、总览:五篇 How-to 指南的主题与适用场景
hypothesis/docs/how-to/index.rst是 Hypothesis 文档中 "How-to guides" 部分的索引页,明确说明这些页面是"在特定场景中应用 Hypothesis 的实用指南,每一页都回答一个关于使用 Hypothesis 的具体问题"。该索引指向以下五篇指南:
| 指南文件 | 核心问题 | 典型使用场景 |
|---|---|---|
suppress-healthchecks.rst | 如何在全局(或全部)范围内抑制健康检查 | 交互式原型开发、已知性能瓶颈、pytest 测试套件全局配置 |
type-strategies.rst | 如何为返回策略的函数编写类型提示 | 自定义策略函数、类型检查工具(mypy、pyright)集成 |
custom-database.rst | 如何编写自定义 Hypothesis 数据库 | 需要将失败用例持久化到 SQLite、Redis 等特定后端 |
detect-hypothesis-tests.rst | 如何动态判断一个测试函数是否由 Hypothesis 定义 | 测试框架集成、插件开发、批量分析测试集合 |
external-fuzzers.rst | 如何与外部模糊测试器(如 Atheris、python-afl)协同工作 | 对原生 C 扩展做覆盖率引导的模糊测试 |
以下各节将逐一深入讲解。
二、全局抑制健康检查(Suppress Health Checks)
2.1 什么是 HealthCheck
Hypothesis 有时会主动抛出HealthCheck,用来提示你的测试可能比预期更低效、更慢,或者生成有效测试用例的概率很低,甚至存在静默的性能退化。从源码hypothesis/src/hypothesis/_settings.py的HealthCheck枚举(_settings.py#L213-L252)可以看出:
- 健康检查是主动警告而非错误——Hypothesis 鼓励你在评估"该检查不会造成问题"或"修复底层问题不值得"时将其抑制;
- 除
HealthCheck.function_scoped_fixture和HealthCheck.differing_executors外,其余健康检查警告的都是性能问题而非正确性错误; - 健康检查可以通过
settings.suppress_health_check配置项禁用,传入suppress_health_check=list(HealthCheck)可抑制全部健康检查。
例如HealthCheck.filter_too_much表示你的测试通过assume()或.filter()过滤掉了太多输入,导致生成有效用例效率低下。
2.2 使用 Profile 在全局抑制特定健康检查
如果你不想关心某一类健康检查,可以通过settings.register_profile和settings.load_profile注册并加载一个 settings 配置文件。将以下代码放在任何在测试运行前被加载的文件中(如果使用 pytest,可放在conftest.py中):
from hypothesis import HealthCheck, settings settings.register_profile( "my_profile", suppress_health_check=[HealthCheck.filter_too_much] ) settings.load_profile("my_profile")这个 profile 会为所有测试抑制HealthCheck.filter_too_much。例外情况是:如果某个测试通过@settings显式设置了不同的suppress_health_check值,那么 profile 中的值会被局部的 settings 值覆盖。
2.3 抑制全部健康检查(含警告)
!!! warning "强烈建议" 我们强烈建议你按需逐个抑制健康检查,而不是一刀切地全部抑制。有几个健康检查检测的是可能为你节省数小时调试时间的微妙交互,例如HealthCheck.function_scoped_fixture(检测@given测试使用了函数作用域的 pytest fixture,实际重置频率与用户预期不符)和HealthCheck.differing_executors(检测同一个@given测试被多个不同的执行器重复执行)。
如果你确实想抑制所有健康检查(例如为了加快交互式原型开发的速度),可以这样做:
from hypothesis import HealthCheck, settings settings.register_profile("my_profile", suppress_health_check=list(HealthCheck)) settings.load_profile("my_profile")2.4 底层实现:register_profile 与 load_profile
从源码看(_settings.py#L1119-L1199),register_profile将 profile 存于内部的settings._profiles字典中;若同名 profile 已存在会被覆盖;若注册的名字恰好是当前激活的 profile,则改动会立即生效、无需重新加载。load_profile则将指定 profile 设为当前 profile,并更新内部的default_variable默认值。get_profile(name)可按名字取回已注册的 profile,未注册则抛出InvalidArgument。此外,register_profile的签名还支持parent参数,即可以基于另一个 profile 派生新 profile(例如settings.register_profile("ci", parent=settings.default, max_examples=1000))。
三、为策略编写类型提示(Type Hints for Strategies)
3.1 SearchStrategy 与 reveal_type
Hypothesis 为所有策略以及所有返回策略的函数提供了类型提示。SearchStrategy是策略的类型,它以生成的值的类型为泛型参数(详见strategies.py#L255-L261中的类定义:"SearchStrategy只在公开 API 中用于类型注解,例如编写-> SearchStrategy[Foo];请不要继承或直接实例化此类")。
可以用reveal_type(mypy/pyright 的内置诊断函数)验证:
from hypothesis import strategies as st reveal_type(st.integers()) # SearchStrategy[int] reveal_type(st.lists(st.integers())) # SearchStrategy[list[int]]3.2 为返回策略的函数标注类型
你可以用SearchStrategy为返回策略的函数编写类型提示:
from hypothesis import strategies as st from hypothesis.strategies import SearchStrategy # returns a strategy for "normal" numbers def numbers() -> SearchStrategy[int | float]: return st.integers() | st.floats(allow_nan=False, allow_infinity=False)这里值得指出策略(strategy)与返回策略的函数(function that returns a strategy)之间的区别:
st.integers是一个函数,它返回一个策略,该策略的类型是SearchStrategy[int];- 因此函数
st.integers的类型是Callable[..., SearchStrategy[int]]; - 而值
s = st.integers()的类型则是SearchStrategy[int]。
3.3 为 @st.composite 定义的策略标注类型
当你为用@st.composite定义的策略编写类型提示时,请使用返回值本身的类型(而不是SearchStrategy):
@st.composite def ordered_pairs(draw) -> tuple[int, int]: n1 = draw(st.integers()) n2 = draw(st.integers(min_value=n1)) return (n1, n2)这里的ordered_pairs是一个被@st.composite包装的策略构造器,draw是其内部注入的绘制函数;返回的tuple[int, int]就是策略生成值的类型。
3.4 SearchStrategy 的协变(covariance)
SearchStrategy是**协变(covariant)**的,即:如果B < A(B 是 A 的子类型),那么SearchStrategy[B] < SearchStrategy[A](策略类型同样保持子类型关系)。换句话说,策略st.from_type(Dog)是策略st.from_type(Animal)的子类型。这意味着你可以安全地把一个生成Dog的策略传给期望SearchStrategy[Animal]参数的函数。
四、编写自定义 Hypothesis 数据库(Custom Database)
4.1 数据库在 Hypothesis 中的角色
Hypothesis 会自动把测试失败用例保存到settings.database指定的数据库中;下次运行同一测试时,Hypothesis 会在Phase.reuse阶段从数据库重放这些失败(源码文档见database.py#L172-L229)。数据库本质上是一个"bytes 到 bytes 集合的简单映射"(mapping of bytes to sets of bytes),可以把它理解成"永远不需要失效的缓存"——升级 Hypothesis 版本或修改测试时条目可能被透明丢弃,因此不要依赖数据库保证正确性;要确保某个输入一定会被尝试,请使用@example。
4.2 实现 ExampleDatabase 的三个必需方法
要自定义ExampleDatabase,你需要实现save、fetch、delete三个方法。下面是文档给出的、以 SQLite 为后端存储的完整示例:
import sqlite3 from collections.abc import Iterable from hypothesis.database import ExampleDatabase class SQLiteExampleDatabase(ExampleDatabase): def __init__(self, db_path: str): self.conn = sqlite3.connect(db_path) self.conn.execute(""" CREATE TABLE examples ( key BLOB, value BLOB, UNIQUE (key, value) ) """) def save(self, key: bytes, value: bytes) -> None: self.conn.execute( "INSERT OR IGNORE INTO examples VALUES (?, ?)", (key, value), ) def fetch(self, key: bytes) -> Iterable[bytes]: cursor = self.conn.execute("SELECT value FROM examples WHERE key = ?", (key,)) yield from [value[0] for value in cursor.fetchall()] def delete(self, key: bytes, value: bytes) -> None: self.conn.execute( "DELETE FROM examples WHERE key = ? AND value = ?", (key, value), )对照源码(database.py#L234-L253)可确认三个方法的语义约定:
save(key, value):把value保存到key下;如果value已存在,静默无操作;fetch(key):返回匹配该 key 的所有 value 的可迭代对象;delete(key, value):从key中移除value;若不存在则静默无操作。
4.3 可选方法 move 及其默认行为
数据库类不要求实现ExampleDatabase.move。默认的move实现是:先在旧 key 上delete该 value,再在新 key 上save该 value(源码见database.py#L255-L269)。如果后端存储提供了更高效的移动操作,你可以重写move来覆盖默认行为。注意:默认实现中若src == dest,则直接执行save(src, value)并返回。
4.4 变更监听(Change Listening)扩展
为了在数据库类中支持变更监听(change listening),每当 value 在后端存储中被保存、删除或移动时,你应当调用ExampleDatabase._broadcast_change。如何追踪变更取决于数据库类的具体实现——例如在DirectoryBasedExampleDatabase中,Hypothesis 通过watchdog安装文件系统监视器来广播变更事件。
两个有用的相关方法是ExampleDatabase._start_listening与ExampleDatabase._stop_listening,数据库类可以重写它们,以得知何时开始或停止昂贵的监听操作。需要说明的是:虽然当前没有任何 Hypothesis 核心功能强制要求变更监听,但HypoFuzz 依赖该能力(见database.py#L203-L206的注释);所有数据库都支持变更监听,自定义数据库若想与依赖变更监听的特性兼容,就需要实现它。相关的监听接口还包括add_listener、remove_listener、clear_listeners。
五、检测 Hypothesis 测试(Detect Hypothesis Tests)
5.1 通过 is_hypothesis_test 检测
最直接的方式是使用is_hypothesis_test:
from hypothesis import is_hypothesis_test @given(st.integers()) def f(n): ... assert is_hypothesis_test(f)该方法对有状态测试同样适用:
from hypothesis import is_hypothesis_test from hypothesis.stateful import RuleBasedStateMachine class MyStateMachine(RuleBasedStateMachine): ... assert is_hypothesis_test(MyStateMachine.TestCase().runTest)从源码(detection.py)看,is_hypothesis_test的判断逻辑是:若传入的是绑定方法(MethodType),则递归检查其底层函数;否则检查该对象是否带有is_hypothesis_test属性且为真。@given装饰器和有状态测试的runTest方法都会被标记该属性。
5.2 通过 pytest 标记检测
如果你在使用 pytest,Hypothesis 的 pytest 插件会自动给所有 Hypothesis 测试打上@pytest.mark.hypothesis标记。你可以使用node.get_closest_marker("hypothesis")或类似方法来检测该标记是否存在。源码证据见_hypothesis_pytestplugin.py#L439(item.add_marker("hypothesis"))。这在编写自定义 pytest 插件、测试收集器或批量分析工具时非常实用。
六、与外部模糊测试器协同工作(External Fuzzers)
6.1 为什么需要 fuzz_one_input
有时你希望把传统的模糊测试器(如 python-afl、Google 的 Atheris)对准自己的代码,以获得对原生 C 扩展的覆盖率引导探索。这类工具链通常远不如属性测试库成熟,因此你可以用 Hypothesis 的策略描述输入数据,用其世界级的收缩(shrinking)与可观测性(observability)工具来处理结果。这正是本指南的价值所在。
!!! note "关于 HypoFuzz" 如果你已经拥有 Hypothesis 测试并想对它们做模糊测试,或者目标是纯 Python 代码,我们强烈推荐使用专门为此构建的HypoFuzz。本节讨论的是使用外部模糊测试器编写传统 "fuzz harness",仅借助 Hypothesis 的部分能力。
为支持该工作流,Hypothesis 暴露了fuzz_one_input方法:它接收一个字节串(bytestring),将其解析为一个测试用例(test case),并执行对应的测试一次。这意味着你可以把每一个 Hypothesis 测试当作传统模糊测试目标,直接把fuzz_one_input交给模糊测试器驱动。示例如下:
from hypothesis import given, strategies as st @given(st.integers()) def test_ints(n): pass # this parses the bytestring into a test case using st.integers(), # and then executes `test_ints` once. test_ints.hypothesis.fuzz_one_input(b"\x00" * 50)6.2 fuzz_one_input 的生命周期语义
注意fuzz_one_input绕过了标准的测试生命周期。在标准测试运行中,Hypothesis 负责管理测试生命周期(例如在各个Phase之间移动);而fuzz_one_input则独立于该生命周期,只执行单个测试用例。它与@settings等特性的交互详见源码文档(core.py#L1733-L1779),要点如下:
- 根据传入的 buffer,有三种结果:
- 字节串无效(例如太短,或被
assume/.filter过滤掉)→ 返回None; - 字节串有效且测试通过 → 返回一个规范化并剪枝后的字节串,可用它重放该测试用例(供变异型模糊测试器提升性能,可安全忽略);
- 测试失败(抛出了异常)→ 将剪枝后的 buffer 加入 Hypothesis 示例数据库,并重新抛出该异常。你只需运行测试套件,即可复现、最小化并去重所有通过模糊测试发现的失败。
- 字节串无效(例如太短,或被
fuzz_one_input只会记录"对已知失败是有效收缩(valid shrinks)"的失败输入,因此数据库写入开销介于常数与 log(N) 之间而非线性;但该追踪只在持久化模糊测试进程内有效,对于 forkserver 型模糊测试器,建议主运行使用database=None,需要分析失败时再启用数据库重放。- 输入/输出字节串的解释方式与当前 Hypothesis 版本及测试所用策略强相关(与数据库和
@reproduce_failure同理)。 - 与
@settings的交互:fuzz_one_input只使用足够驱动测试的 Hypothesis 内部机制,大多数 settings 在该模式下不生效。官方建议:模糊测试前先用常规方式运行测试以获得健康检查收益,模糊测试后再用常规方式运行以重放、收缩、去重和报告发现的错误;settings.database仍会被使用——把失败加入数据库并在下次运行时重放,是推荐的报告机制,也是应对 "fuzzer taming" 问题的方式。
6.3 实战示例:结合 Atheris
下面是使用fuzz_one_input与 Atheris(基于 libFuzzer 的覆盖率引导模糊测试器)配合的完整示例——它生成任意 JSON 值并验证json.dumps不会出错:
import json import sys import atheris from hypothesis import given, strategies as st @given( st.recursive( st.none() | st.booleans() | st.integers() | st.floats() | st.text(), lambda j: st.lists(j) | st.dictionaries(st.text(), j), ) ) def test_json_dumps_valid_json(value): json.dumps(value) atheris.Setup(sys.argv, test_json_dumps_valid_json.hypothesis.fuzz_one_input) atheris.Fuzz()仅靠 Atheris 的FuzzDataProvider接口生成合法的 JSON 对象会困难得多——这正是 Hypothesis 策略的优势所在。你还可以使用atheris.instrument_all或atheris.instrument_imports为 Atheris 添加覆盖率插桩(详见 Atheris 官方文档)。
七、五篇指南的配套阅读
- 健康检查与 settings 的完整参数说明见 settings 文档 及源码
hypothesis/src/hypothesis/_settings.py; - 数据库相关测试用例见
hypothesis/tests/cover/test_database_backend.py与hypothesis/tests/watchdog/test_database.py; fuzz_one_input的测试用例见hypothesis/tests/cover/test_fuzz_one_input.py;- 状态机与有状态测试详见 stateful 文档。
通过以上五篇 How-to,你可以快速解决日常使用 Hypothesis 中"抑制告警、类型标注、失败持久化、测试识别、接入外部模糊测试"这五类高频问题。
- 测试
- 开发工具
【免费下载链接】hypothesis
The property-based testing library for Python
相关推荐
Buildah容器健康检查:自定义检测脚本
Buildah容器健康检查:自定义检测脚本 你是否曾遇到过容器启动正常但实际服务不可用的情况?作为容器镜像构建工具,Buildah不仅支持OCI(Open Co
云原生Lwt快速入门:5分钟上手OCaml并发I/O编程 🚀
Lwt快速入门:5分钟上手OCaml并发I/O编程 🚀 想要在OCaml中轻松处理并发I/O操作吗?Lwt(Lightweight Threads)是OCam
Hypothesis测试框架与Pyright:Python类型检查新体验
Hypothesis测试框架与Pyright:Python类型检查新体验 在Python开发中,类型安全和测试效率是提升代码质量的关键环节。Hypothesis
测试开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考