系列文章:Python 奇技淫巧 #013
很多测试之所以“总能通过”,并不是因为代码真的稳,而是因为你喂给它的输入太正常了:长度刚好、顺序理想、字符干净、数字不极端、状态变化也很规矩。这样的测试当然有价值,但它们经常只能证明“你手里这几个样例没问题”,证明不了“这段逻辑对一整类输入都靠谱”。Hypothesis 的厉害之处,就在于它把测试从“手工举例”升级成“自动探索输入空间”——你描述应该始终成立的性质,它负责自动生成样例、主动撞边界、缩小失败输入,最后把 bug 逼到一个最小可复现样例上。 这件事,对工具库、API 服务、数据清洗、AI 应用、状态机流程都很有杀伤力。
很多团队并不是不测,而是测法天然有盲区:
所以真正的问题不是:
“我还能再补几个测试样例?”
而是:
“我能不能描述一条应该始终成立的性质,然后让工具替我去撞那些我没想到的输入?”
这正是 Hypothesis 的价值。

上面这张图是我自制的 SVG 信息图。SVG 本质上是用代码描述的矢量图,放大不糊、改字方便、适合版本管理,尤其适合技术文章里解释流程、结构和能力边界。像 Hypothesis 这种偏“测试方法升级”的工具,用 SVG 来讲会比截图更清楚。
Hypothesis 官方文档首页给它的定位非常直接:
Hypothesis is the property-based testing library for Python.
官方对它的进一步描述也很关键:你写的是“对某个输入范围内都应该成立的测试”,然后让 Hypothesis 自动去选择要检查哪些输入,包括你自己未必会主动想到的边界情况。
这意味着它和传统“手写固定样例”的测试思路不太一样:

Hypothesis 最值得记住的几个能力点,大概是:
@given(...)hypothesis.strategies@settings(...)RuleBasedStateMachinepytest、unittest 等常见测试方式协同这里最重要的一句不是“它会随机造数据”,而是:
Hypothesis 不是随机测试玩具,而是一套“性质驱动 + 自动生成 + 最小反例收缩”的测试方法。

很多人最开始写测试,是这样想的:
def test_split_ok():
assert split_into_batches([1, 2, 3, 4], 2) == [[1, 2], [3, 4]]
def test_split_tail():
assert split_into_batches([1, 2, 3], 2) == [[1, 2], [3]]
这当然没错,但问题也很明显:
size、极小 size 等边界而 Hypothesis 的思路是换个问法:
不去问“这两个样例结果对不对”,而去问“对于任意合法输入,这个函数有没有某些始终成立的性质?”
一旦你问对了问题,很多本来很难系统覆盖的边界,就能自动被扫出来。
来看一个非常常见的工具函数:把列表切成固定大小的小批次。
def split_into_batches(items: list[int], size: int) -> list[list[int]]:
return [items[i : i + size] for i in range(0, len(items) - 1, size)]
这段代码乍一看很像真的,很多人甚至肉眼扫一遍都不一定立刻发现问题。但它其实有一个隐藏 bug:
如果你手工写样例,很容易刚好写到几个“碰巧没暴露问题”的输入。更稳的做法是直接写性质:
size对应的 Hypothesis 测试可以这样写:
from hypothesis import given, strategies as st
@given(
items=st.lists(st.integers()),
size=st.integers(min_value=1, max_value=20),
)
def test_split_into_batches_preserves_data(items, size):
batches = split_into_batches(items, size)
flattened = [x for batch in batches for x in batch]
assert flattened == items
assert all(len(batch) <= size for batch in batches)
assert all(len(batch) > 0 for batch in batches)
这里真正妙的地方不在于语法,而在于你已经把测试目标从:
[1,2,3,4] 和 [1,2,3] 这两个例子”升级成了:
如果这个实现真的有问题,Hypothesis 往往会很快给出一个失败输入,而且不满足于只给你一个巨大的脏例子,它还会继续 shrink。
你最后看到的,很可能是这种级别的反例:
Falsifying example: test_split_into_batches_preserves_data(
items=[0],
size=1,
)
这就是真正让人上头的地方:
它不只是替你找 bug,还会努力把 bug 压缩成一个最容易理解、最容易复现的最小失败样例。
这比“随机扔一大堆数据,然后报一个你根本看不懂的失败案例”强太多了。
@given 和 strategy,才是 Hypothesis 的核心入口Hypothesis 的写法并不复杂。最常见的入口就是:
@given(...)from hypothesis import strategies as st其中 @given 表示“把生成出来的数据喂给测试函数”,而 st 则是用来描述数据空间的。
比如这些都是很常用的策略:

一个非常重要的思维变化是:
你不是在“列出输入”,而是在“描述输入空间”。
这两者看起来只差一点,测试能力却差很多。
map、filter、composite 才是工程价值放大的地方官方文档专门有一节讲 strategy 的适配方式,核心就是:
.map().filter()@st.composite这部分很关键,因为真实项目里的输入通常不是“一个整数”这么简单,而是:
来看一个更贴近 AI 服务项目的例子:生成聊天请求。
from hypothesis import given, strategies as st
@st.composite
def chat_requests(draw):
message_count = draw(st.integers(min_value=1, max_value=6))
messages = []
for _ in range(message_count):
role = draw(st.sampled_from(["system", "user", "assistant"]))
content = draw(st.text(min_size=1, max_size=200))
messages.append({"role": role, "content": content})
temperature = draw(
st.floats(
min_value=0,
max_value=2,
allow_nan=False,
allow_infinity=False,
)
)
return {"messages": messages, "temperature": temperature}
@given(chat_requests())
def test_prompt_builder_never_drops_message(payload):
prompt = "\n".join(f"{m['role']}: {m['content']}" for m in payload["messages"])
for msg in payload["messages"]:
assert msg["content"] in prompt
这类测试的意义,不在于它能替代全部业务测试,而在于它可以帮你快速验证:
这里有一个经验判断:
这两类测试不是互相替代,而是互相补位。
Hypothesis quickstart 里明确提到:一个 Hypothesis 测试仍然是普通 Python 函数,所以 pytest 或 unittest 都可以像平常一样收集和运行它。
这点非常重要,因为它意味着:
一个很自然的组合方式是:
import pytest
from hypothesis import given, strategies as st
@pytest.mark.slow
@given(st.lists(st.integers(), min_size=1, max_size=50))
def test_sorted_result_is_ordered(values):
result = sorted(values)
assert result == sorted(result)
如果你已经把项目测试组织在 pytest 下,那么引入 Hypothesis 最舒服的姿势不是“新开一套测试体系”,而是:
在已有 pytest 工程里,挑那些最容易出边界 bug 的函数,增量补上 property-based tests。
这比全盘迁移轻得多,也实用得多。

这张图可以把 Hypothesis 的核心工作流压缩成一句话:
先定义性质,再描述输入空间,交给 Hypothesis 自动生成样例;一旦失败,就收缩成最小反例,再回到 pytest/CI 里稳定复现。
它最值钱的地方,不是“随机生成很多数据”,而是把“生成 → 发现 → 收缩 → 复现”这条链完整打通了。
@settings 不只是调参数,它在决定你怎么把 Hypothesis 用进工程里官方文档对 @settings(...) 的说明很实在:你可以用它控制:
max_examples)derandomize)最常见的用法大概长这样:
from hypothesis import given, settings, strategies as st
@settings(max_examples=300, deadline=None)
@given(st.text(), st.integers(min_value=1, max_value=50))
def test_chunking_roundtrip(text, size):
chunks = [text[i : i + size] for i in range(0, len(text), size)]
assert "".join(chunks) == text
这里的几个工程理解很重要:

官方文档还提到 settings profile。这件事我很建议在工程里用起来,因为很适合区分:
如果你已经有 pytest 的测试分层,再配合 Hypothesis settings profile,整套体验会非常顺。
Hypothesis 官方文档里还有一个很强、但很多人没认真学的能力:stateful testing。
官方对它的描述很准确:让 Hypothesis 不只选择“值”,还选择“动作”。也就是说,它不只是给你造输入,还会自动组合操作序列。
这对什么场景特别有用?
核心入口通常是 RuleBasedStateMachine,再配合:
@rule(...)@invariant(...)Bundle一个极简例子:
from hypothesis import strategies as st
from hypothesis.stateful import RuleBasedStateMachine, invariant, rule
class CounterMachine(RuleBasedStateMachine):
def __init__(self):
super().__init__()
self.values = []
self.total = 0
@rule(x=st.integers(min_value=0, max_value=100))
def append_value(self, x):
self.values.append(x)
self.total += x
@invariant()
def total_matches_values(self):
assert self.total == sum(self.values)
当然,真实项目往往会比这复杂得多,但它提供的方向非常清楚:
如果 bug 来自“操作顺序”和“状态转换”,而不是一个静态输入,那么单纯样例测试就不够,stateful testing 会更像对的问题。
这也是很多缓存、任务调度、业务状态流转 bug 的高发区。
如果你只是想随机生成一堆输入,却没有描述明确性质,那测试价值会迅速下降。
正确思路是先问:
filter() 和 assume() 用太狠官方文档也提醒过,过度过滤会让输入生成效率变差。很多时候,更好的方式是:
map() / composite 把结构一次性建对filter()比如只断言“函数返回了 list”,这几乎没有测试价值。
更有用的性质通常是:
最稳的做法永远是:
Hypothesis 很强,但真正高效的用法不是“到处乱上”,而是优先砸那些你知道自己最容易漏边界的地方。
如果你现在已经在用 pytest,我会建议这样落地:
优先考虑:
不要先想“我要生成什么奇怪数据”,而是先想:
例如只在 tests/property/ 或 tests/unit/ 里先放几条 property-based tests,不要一开始就推倒重来。
别一开始就上状态机,但一旦你的 bug 主要出在流程顺序,那就别继续死磕静态样例了。
很多人把测试能力理解成“样例写得够不够多”,但我越来越觉得,真正决定测试上限的,是你有没有能力从“样例思维”切到“性质思维”。
而 Hypothesis 恰好就在做这件事:
所以我更愿意这样定义它:
Hypothesis 不是帮你多写测试,而是帮你把测试从“举例说明”推进到“系统证明”。
这就是它真正值钱的地方。
from hypothesis import given, strategies as st
def reverse_twice(text: str) -> str:
return text[::-1][::-1]
@given(st.text())
def test_reverse_twice_roundtrip(text):
assert reverse_twice(text) == text
如果你已经在用 pytest,直接执行:
pip install hypothesis pytest
pytest
接着你就可以把最容易藏边界 bug 的那几个函数,逐步改写成 property-based tests。
学 Hypothesis,别只学 @given 的语法。
真正要学会的是:
当你这样理解 Hypothesis,它就不再只是“自动造数据”,而是“帮你主动挖边界 bug 的测试引擎”。