Hypothesis 入门

本页介绍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、格式化器或编译器处理语法有效的代码时不会崩溃。
  • 原文链接中还有更多例子。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容