Skip to content

第 18 章 · 属性基测试与 Hypothesis

本章目标:理解 property-based testing 与 example-based 的本质差异,掌握 Hypothesis 的策略、设置与反例缩小机制,并学会与 parametrize 组合使用。

18.1 从"举例"到"性质"

前面所有章节的测试都是 example-based:人挑几组输入,写下期望输出。它的盲区很明显——你只能测到你想得到的输入。sorted[]、[1]、[2,1] 测着都好,但谁会想到传一个含 float('nan') 的列表?

property-based 换个问法:不问"这组输入输出是什么",而问"对任意合法输入,哪些性质必须永远成立"。比如排序函数的性质:

  • 输出有序;
  • 输出是输入的一个排列(长度相同、元素多重集相同);
  • 幂等:再排一次结果不变。

Hypothesis 会按策略随机生成成百上千组输入去攻击这些性质,一旦找到反例就报告并固定下来。

bash
pip install hypothesis

18.2 @given 与 strategy 入门

@given 是唯一的核心入口,接收一个或多个 strategy:

python
# test_hypo_basic.py
from hypothesis import given, strategies as st


@given(st.integers())
def test_abs_non_negative(n):
    assert abs(n) >= 0            # 性质:绝对值非负,对任意整数成立


@given(st.text())
def test_encode_decode_roundtrip(s):
    # 性质:编码后再解码应得到原串(roundtrip 性质是最常用的模式)
    assert s.encode("utf-8").decode("utf-8") == s


@given(st.lists(st.integers()))
def test_sorted_properties(xs):
    result = sorted(xs)
    assert len(result) == len(xs)                 # 长度不变
    assert all(result[i] <= result[i + 1] for i in range(len(result) - 1))
    assert sorted(result) == result               # 幂等

常用内置策略速查:

策略生成内容
st.integers(min_value, max_value)整数
st.floats(allow_nan=False)浮点数
st.text(alphabet=..., max_size=...)Unicode 文本
st.lists(elements, min_size, max_size, unique=True)列表/去重列表
st.dictionaries(keys, values)字典
st.one_of(a, b)多策略任选
st.sampled_from([...])从给定集合抽取

复杂输入用 @st.composite 组装依赖关系,用 .filter() / assume() 过滤:

python
from hypothesis import assume, given, strategies as st


@st.composite
def ordered_pair(draw):
    """draw 函数由 Hypothesis 注入,可顺序构造有依赖的值。"""
    lo = draw(st.integers())
    hi = draw(st.integers(min_value=lo))   # 保证 hi >= lo
    return (lo, hi)


@given(st.integers(), st.integers())
def test_div(n1, n2):
    assume(n2 != 0)                        # 丢弃除零用例
    q = n1 / n2
    assert q * n2 == n1 or True            # 示意:浮点误差需另行处理

18.3 @settings 与确定性复现

python
import pytest
from hypothesis import given, settings, strategies as st


@settings(max_examples=50, deadline=None)   # CI 里可调低数量提速;deadline 关闭单例耗时断言
@given(st.lists(st.integers(), min_size=1))
def test_min_is_member(xs):
    assert min(xs) in xs


# 数据库/网络类慢测试建议显式放宽 deadline:
@settings(deadline=500)                     # 单个用例超过 500ms 才算失败
@given(st.text(max_size=100))
def test_indexing(s):
    assert s.find("") == 0                  # find 空串总返回 0

关键机制——shrinking(反例缩小):发现失败后,Hypothesis 不是直接甩给你一坨随机大对象,而是自动尝试更小、更简单的输入,直到找到最小反例。失败输出形如:

text
Falsifying example: test_parse(
    s='0',
)

同时它会把这个反例写进 .hypothesis/examples 目录下的数据库,下次运行时优先重放历史反例——所以回归是确定性的:修好了才绿。

18.4 与 pytest.mark.parametrize 组合

Hypothesis 官方明确支持与参数化叠加:外层枚举"操作",内层生成数据:

python
import pytest
from hypothesis import given, strategies as st


@pytest.mark.parametrize("operation", [reversed, sorted])
@given(st.lists(st.integers()))
def test_operation_preserves_length(operation, lst):
    assert len(list(operation(lst))) == len(lst)

注意规则:@given 必须在最外层(紧贴函数),parametrize 的参数会原样传入。反过来(given 在上、parametrize 在下)是不支持的。

18.5 选型:什么时候值得上 Hypothesis

  • ✅ 解析器/序列化器:roundtrip 性质(encode→decode、dump→load)几乎零成本高收益;
  • ✅ 数学/金融计算:交换律、结合律、单调性等代数性质;
  • ✅ 状态ful 系统:RuleBasedStateMachine 可以随机生成操作序列做模型测试(进阶话题);
  • ❌ 纯 IO 编排代码(调第三方 API 的胶水层):没有可验证的纯函数性质,mock 后意义不大。

成本提示

默认每个属性测试跑 100 个例子,大型套件会显著变慢。常见做法是本地全量、CI 用 --hypothesis-profile=ci 调低 max_examples,每晚定时任务再跑高探索档位。

本章小结

  • property-based 测试验证"任意合法输入下不变量恒成立",专治 example-based 的想象盲区;
  • @given(strategy) + 内置 strategies 覆盖绝大多数类型,composite/filter/assume 处理复杂约束;
  • @settings(max_examples, deadline) 控制强度与耗时;失败反例自动 shrinking 并持久化复放;
  • @given 必须最外层,可与 parametrize 叠加形成"操作 × 数据"矩阵;
  • 最适合解析、算法、数值类纯逻辑;IO 胶水层收益有限。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. property-based testing 相比 example-based 的核心优势是?

2. 在 Hypothesis 中,assume(cond) 的作用是?

3. 关于 shrinking 反例缩小机制,正确的说法是?

4. @given 与 @pytest.mark.parametrize 同时使用时,正确的装饰器顺序是?

🛠️ 动手实践

  1. 为 JSON 序列化器写 roundtrip 属性测试:json.loads(json.dumps(x)) == x,用 st.recursive 生成嵌套结构。
  2. 给一个手写的 parse_duration("3h30m") 函数找茬:定义"秒数往返一致""非法输入必抛异常"两条性质,让 Hypothesis 攻击它,记录它抓到的第一个 bug。
  3. 把第 15 章的参数化测试改造成 parametrize + given 组合,并用 @settings(max_examples=20) 控制运行时间。

下一章把镜头拉远,聊聊整个 Web 服务的集成测试策略