本页介绍Hypothesis的两个基本部分——@given 和策略(strategy),并展示如何用Hypothesis测试选择排序实现。
安装Hypothesis
首先安装:
pip install hypothesis
定义简单测试
Hypothesis测试由两部分构成:@given,以及传给它的策略。一个简单例子:
from hypothesis import given, strategies as st @given(st.integers()) def test_is_integer(n): assert isinstance(n, int)
添加 @given 装饰器,就把函数变为Hypothesis测试。传入 integers() 表示测试时,为参数 n 生成随机整数。
直接调用即可运行:
from hypothesis import given, strategies as st @given(st.integers()) def test_is_integer(n): print(f"called with {n}") assert isinstance(n, int) test_is_integer()
注意,我们没有给 n 传值;Hypothesis负责生成。输出类似:
called with 0 called with -18588 called with -672780074 called with 32616 ...
测试排序算法
假设实现了简单的选择排序:
# contents of example.py from hypothesis import given, strategies as st def selection_sort(lst): result = [] while lst: smallest = min(lst) result.append(smallest) lst.remove(smallest) return result
希望确认实现正确。组合 integers() 和 lists() 策略,编写以下测试:
... @given(st.lists(st.integers())) def test_sort_correct(lst): print(f"called with {lst}") assert selection_sort(lst.copy()) == sorted(lst) test_sort_correct()
运行 test_sort_correct 时,Hypothesis使用 lists(integers()) 生成随机整数列表。原文建议运行 python example.py,观察生成的列表,并确认该测试能够通过。
加入浮点数
这是个好的起点,但 selection_sort 也应能排序浮点数列表。要生成同时可能含整数或浮点数的列表,可以修改策略:
# changes to example.py @given(st.lists(st.integers() | st.floats())) def test_sort_correct(lst): pass
管道运算符 | 接受两个策略,返回一个从任意一个策略生成值的新策略。因此 integers() | floats() 能生成整数或浮点数。
防止floats()生成nan
只有整数时测试通过,加入浮点数后却会失败。如果运行 python example.py,很可能(但并非总是,因为是随机测试)会看到Hypothesis报告反例。原作者得到的是 [1.0, nan, 0],你的结果可能不同。
问题在于,存在 nan 时排序没有良好定义。因此可以决定测试时不生成它。传入 floats(allow_nan=False):
# changes to example.py @given(st.lists(st.integers() | st.floats(allow_nan=False))) def test_sort_correct(lst): pass
在原文示例中,这样测试就能通过。
具有多个参数的测试
向 @given 传入多个策略,就可以给测试多个参数:
from hypothesis import given, strategies as st @given(st.integers(), st.lists(st.floats())) def test_multiple_arguments(n, lst): assert isinstance(n, int) assert isinstance(lst, list) for f in lst: assert isinstance(f, float)
关键字参数
也可以通过关键字参数传策略:
@given(lst=st.lists(st.floats()), n=st.integers()) # <-- changed def test_multiple_arguments(n, lst): pass
虽然改变了策略在 @given 中的顺序,但关键字明确指定了对应参数,因此测试含义不变。通常可以把位置参数和关键字参数理解为转发给测试函数的参数。
运行Hypothesis测试
有几种方式:
- 像
test_is_integer()那样显式调用。测试仍是普通函数,只是@given负责生成并传入参数值。 - 让pytest等测试运行器发现它,只要函数名以
test_开头。
具体运行时,Hypothesis会生成100个随机输入,对每个输入执行函数体,报告产生的异常。示例数量由 max_examples 设置控制,默认100。
什么时候使用Hypothesis和基于性质的测试
基于性质的测试是对单元测试的有力补充,并不总能替代它。若难以想到可测试的性质,原文建议:
- 寻找往返性质,如编码/解码、序列化/反序列化。这类测试往往强大且容易写。
- 查找现有测试中的
@pytest.mark.parametrize,它有时意味着可以改用策略。例如@pytest.mark.parametrize("n", range(0, 100))可以换成@given(st.integers(0, 100 - 1))。 - 直接用Hypothesis生成结构正确的随机输入调用代码。它找到崩溃的频率可能出乎意料,对单一入口覆盖大量底层代码的项目尤其有价值。
其他性质例子:
- 优化实现与较慢但明确正确的实现等价。
- 金融系统中的一系列交易始终平衡,资金不会消失。
- 次数为
n的多项式,其导数次数为n - 1。 - 类型检查器、linter、格式化器或编译器处理语法有效的代码时不会崩溃。
- 原文链接中还有更多例子。











暂无评论内容