编写测试是每个开发者的"必修课",但传统的单元测试方法总有一个致命弱点——你只能测试你想到的情况!
有没有想过让测试自己"思考",主动帮你发现那些你根本没考虑到的边界情况?这正是属性测试(Property-based Testing)的魅力所在!而Hypothesis就是Python世界中最强大的属性测试库。(真的超好用!)
本文将带你入门Hypothesis,用最少的代码获得最强大的测试覆盖。不需要复杂的理论,直接从实际例子学起!
在开始前,我们先区分两种测试思路:
示例测试(传统方式):你提供具体输入和预期输出 python assert add(2, 3) == 5
属性测试:你描述函数应该满足的性质,测试框架自动生成数据验证 python # 假设:任何数加0等于它自己 @given(x=integers()) def test_add_zero(x): assert add(x, 0) == x
属性测试的厉害之处在于——它会尝试各种各样的输入(包括极端情况),帮你找出代码中的缺陷!
超简单!
bash pip install hypothesis
Hypothesis的核心是@given装饰器和各种数据生成策略。来看个最基本的例子:
```python from hypothesis import given from hypothesis.strategies import integers
@given(a=integers(), b=integers()) def test_addition_commutative(a, b): assert a + b == b + a ```
运行这个测试时,Hypothesis会自动生成大量整数对,验证加法的交换律是否成立。(它默认会生成100个测试用例!)
Hypothesis提供了丰富的数据生成策略,它们都在hypothesis.strategies模块中:
python from hypothesis.strategies import ( integers, floats, text, lists, dictionaries, booleans, datetimes )
几个常用策略的简单示例:
```python
@given(integers(min_value=-1000, max_value=1000)) def test_with_bounded_integers(x): # 测试内容...
@given(floats(allow_nan=False, allow_infinity=False)) def test_with_nice_floats(x): # 测试内容...
@given(text()) def test_with_text(s): # 测试内容...
@given(lists(integers(), max_size=100)) def test_with_lists_of_integers(xs): # 测试内容... ```
我最喜欢的一点是,这些策略是可组合的!你可以构建复杂的数据结构:
```python
complex_dict = dictionaries( keys=text(min_size=1), values=lists(integers()) )
@given(complex_dict) def test_with_complex_data(d): # 测试内容... ```
假设我们有一个简单的排序函数:
python def sort_list(items): return sorted(items)
传统测试可能这样写:
python def test_sort_traditional(): assert sort_list([3, 1, 2]) == [1, 2, 3] assert sort_list([]) == [] assert sort_list([1]) == [1]
但这只测试了3种情况!用Hypothesis可以测试成千上万种情况:
```python from hypothesis import given from hypothesis.strategies import lists, integers
@given(lists(integers())) def test_sort_properties(items): sorted_items = sort_list(items)
```
这样,Hypothesis会自动生成各种列表(空列表、单元素列表、有重复元素的列表、非常长的列表等),彻底测试我们的排序函数!
Hypothesis有个超赞的功能:当它找到测试失败的用例时,会自动尝试简化该用例,找到能导致失败的最简单输入!
让我们看一个有bug的代码:
python def mean(numbers): return sum(numbers) / len(numbers)
这个函数在空列表时会出错。用Hypothesis测试:
python @given(lists(floats(allow_nan=False, allow_infinity=False))) def test_mean(numbers): result = mean(numbers) # 均值应该在最小值和最大值之间 if numbers: # 我们加了判断,但mean函数里没有! assert min(numbers) <= result <= max(numbers)
运行时,Hypothesis会找到失败案例(空列表),并报告:
Falsifying example: test_mean(numbers=[]) ZeroDivisionError: division by zero
而不是给你一个复杂的列表!这叫做"缩小反例"(shrinking),极大地帮助调试。
有时我们需要对生成的数据增加约束,Hypothesis提供了assume()函数:
```python from hypothesis import given, assume from hypothesis.strategies import integers
@given(x=integers(), y=integers()) def test_division(x, y): # 我们需要确保除数不为零 assume(y != 0)
```
不过频繁使用assume会影响测试效率。更好的方法是调整策略:
```python from hypothesis.strategies import integers
nonzero = integers().filter(lambda x: x != 0)
@given(x=integers(), y=nonzero) def test_division_better(x, y): result = x / y # 测试其他性质... ```
当Hypothesis找到失败用例时,它会显示一个"种子",让你可以精确重现这个失败:
Falsifying example: test_something(x=5, y='abc') You can reproduce this example by temporarily adding @seed(12345) before @given.
按照提示添加@seed装饰器,就能重现问题:
```python from hypothesis import given, seed from hypothesis.strategies import integers, text
@seed(12345) # 添加种子以重现特定失败 @given(x=integers(), y=text()) def test_something(x, y): # 测试内容... ```
这对调试非常有用!(保存下来放进CI也很方便!)
在很多情况下,我们需要测试多个参数的组合。Hypothesis提供了几种方法:
python @given( a=integers(), b=text(), c=lists(floats()) ) def test_function(a, b, c): # 测试代码...
```python from hypothesis.strategies import tuples
@given(tuples(integers(), text())) def test_with_tuple(t): a, b = t # 测试代码... ```
```python from hypothesis.strategies import composite
@composite def user_data(draw): name = draw(text(min_size=1)) age = draw(integers(min_value=0, max_value=120)) return {"name": name, "age": age}
@given(user_data()) def test_user_processing(data): # 测试使用用户数据的代码 process_user(data["name"], data["age"]) ```
Hypothesis可以与主流测试框架完美集成:
```python
from hypothesis import given from hypothesis.strategies import integers
def test_addition_commutative(): @given(a=integers(), b=integers()) def check(a, b): assert a + b == b + a
```
运行:pytest test_something.py
```python import unittest from hypothesis import given from hypothesis.strategies import integers
class TestMathProperties(unittest.TestCase): @given(a=integers(), b=integers()) def test_addition_commutative(self, a, b): self.assertEqual(a + b, b + a)
if name == 'main': unittest.main() ```
Hypothesis还可以测试有状态的系统!例如,测试一个简单的栈实现:
```python from hypothesis.stateful import RuleBasedStateMachine, rule, invariant from hypothesis.strategies import integers
class StackMachine(RuleBasedStateMachine): def init(self): super().init() self.stack = []
TestStack = StackMachine.TestCase ```
Hypothesis会自动生成一系列push和pop操作序列,检查是否违反了我们设定的不变量!
使用Hypothesis时需要注意几点:
测试速度:生成大量测试用例可能会变慢,设置合适的max_examples参数 python @settings(max_examples=50) @given(integers()) def test_something(x): # 测试内容...
不确定性处理:处理随机性或外部资源时,需要谨慎 python # 错误示范 @given(integers()) def test_with_randomness(x): # 这会导致不稳定的测试! assert random.random() < 0.99
过滤效率:避免过度使用assume或.filter(),会导致测试效率低下 ```python # 低效 @given(integers().filter(lambda x: x % 100 == 0 and x > 1000 and is_prime(x)))
# 更好的方式 @given(integers(min_value=1000, step=100).filter(is_prime)) ```
让我们测试一个日期差计算函数:
```python from datetime import date, timedelta
def date_diff_in_days(start_date, end_date): """计算两个日期之间的天数差""" delta = end_date - start_date return delta.days ```
用Hypothesis测试:
```python from hypothesis import given from hypothesis.strategies import dates
@given( start=dates(min_value=date(1900, 1, 1), max_value=date(2100, 12, 31)), days=integers(min_value=0, max_value=36500) ) def test_date_diff(start, days): # 创建结束日期 end = start + timedelta(days=days)
```
这个测试会自动覆盖大量日期组合,包括跨世纪、闰年等各种情况!
Hypothesis是一个改变测试思维方式的强大工具!它通过:
开始使用Hypothesis的步骤:
属性测试不是要替代传统单元测试,而是作为强大补充。两者结合使用,能极大提高代码质量和稳定性。
希望这篇教程对你有所帮助!开始尝试Hypothesis,让你的测试更智能,发现更多潜在问题!(真的会让你惊讶它能找出哪些边界情况!)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。