开发者应认真对待代码测试。您可能会使用 pytest 编写单元测试,模拟依赖项,并努力提高代码覆盖率。然而,许 […]
开发者应认真对待代码测试。您可能会使用 pytest 编写单元测试,模拟依赖项,并努力提高代码覆盖率。然而,许多开发者在完成测试套件后,心中可能仍萦绕着一个挥之不去的问题。
“我是否考虑到了所有的边界情况?”
开发者可能会用正数、负数、零和空字符串来测试输入。但那些奇特的 Unicode 字符呢?或者那些非数字(NaN)或无穷大的浮点数?又或者,由空字符串列表组成的列表,甚至是复杂的嵌套 JSON 结构呢?可能的输入空间是巨大的,要想穷尽代码可能出错的无数种方式,尤其是在时间压力下,是极其困难的。
基于属性的测试将这种负担从开发者转移到了工具本身。开发者无需手动挑选测试用例,而是声明一个属性——一个对所有输入都必须成立的真理。Hypothesis 库随后会自动**生成**输入数据(如果需要,可生成数百个),搜寻反例,并且——如果找到一个反例——将其**缩小**到最简单的失败案例,从而极大简化调试过程。
本文将深入探讨基于属性的测试这一强大概念及其在 Hypothesis 中的具体实现。文章将超越简单的函数测试,展示如何有效地测试复杂数据结构和有状态类,以及如何微调 Hypothesis 以实现更健壮、更高效的测试。
基于属性的测试是一种测试方法论,开发者不再为特定的、硬编码的示例编写测试用例,而是定义代码的通用“属性”或“不变量”。一个属性是关于代码行为的高层次陈述,它应该对所有有效输入都成立。随后,开发者可以使用像 Hypothesis 这样的测试框架,它将智能地生成各种输入,并尝试找到一个“反例”——即一个使得开发者所声明的属性不成立的特定输入。
使用 Hypothesis 进行基于属性的测试,其核心特点包括:
基于属性的测试最主要的好处在于它能够发现细微的错误,并显著提升对代码正确性的信心,其效果远超单独使用基于示例的测试。它促使开发者更深入地思考代码的契约和隐含假设。
Hypothesis 在以下场景中表现尤为出色:
decode(encode(x)) 应该等于 x。这非常适合测试处理 JSON 或自定义二进制格式的函数。开发者所需的只有 Python 和 pip。我们将安装 pytest 作为测试运行器,Hypothesis 库本身,以及 pydantic,用于后续的一个高级示例。
(base) tom@tpr-desktop:~$ python -m venv hyp-env (base) tom@tpr-desktop:~$ source hyp-env/bin/activate (hyp-env) (base) tom@tpr-desktop:~$安装 pytest, hypothesis, 和 pydantic
(hyp-env) (base) tom@tpr-desktop:~$ pip install pytest hypothesis pydantic
创建一个新文件夹来存放 Python 代码
(hyp-env) (base) tom@tpr-desktop:~/hypothesis_project$ mkdir hyp-project
Hypothesis 最好与 pytest 等成熟的测试运行器工具结合使用,本文也将采用这种方式。
在这个最简单的例子中,有一个计算矩形面积的函数。它应该接受两个大于零的整数参数,并返回它们的乘积。
Hypothesis 测试的定义依赖于两部分:@given 装饰器和一个传递给该装饰器的策略(strategy)。可以把策略理解为 Hypothesis 将生成数据类型来测试函数的方式。下面是一个简单的例子。首先,定义要测试的函数。
# my_geometry.pydef calculate_rectangle_area(length: int, width: int) -> int:
"""
根据长和宽计算矩形的面积。如果任一维度不是正整数,此函数将引发 ValueError。
"""
if not isinstance(length, int) or not isinstance(width, int):
raise TypeError("长度和宽度必须是整数。")if length <= 0 or width <= 0:
raise ValueError("长度和宽度必须是正数。")
return length * width
接下来是测试函数。
# test_rectangle.pyfrom my_geometry import calculate_rectangle_area
from hypothesis import given, strategies as st
import pytest通过为两个参数使用 st.integers(min_value=1),确保
Hypothesis 只会为函数生成有效输入。
@given(
length=st.integers(min_value=1),
width=st.integers(min_value=1)
)
def test_rectangle_area_with_valid_inputs(length, width):
"""
属性:对于任何正整数的长度和宽度,面积应等于它们的乘积。此测试确保核心乘法逻辑是正确的。 """ print(f"使用有效输入进行测试: length={length}, width={width}") # 正在检查的属性是面积的数学定义。 assert calculate_rectangle_area(length, width) == length * width
将 @given 装饰器添加到函数上,即可将其转换为 Hypothesis 测试。将策略(st.integers)传递给装饰器,表示 Hypothesis 在测试时应为参数生成随机整数,但通过确保任何整数都不能小于一,进一步限制了生成范围。
可以通过以下方式运行此测试。
(hyp-env) (base) tom@tpr-desktop:~/hypothesis_project$ pytest -s test_my_geometry.py=========================================== test session starts ============================================
platform linux -- Python 3.11.10, pytest-8.4.0, pluggy-1.6.0
rootdir: /home/tom/hypothesis_project
plugins: hypothesis-6.135.9, anyio-4.9.0
collected 1 itemtest_my_geometry.py Testing with valid inputs: length=1, width=1
Testing with valid inputs: length=6541, width=1
Testing with valid inputs: length=6541, width=28545
Testing with valid inputs: length=1295885530, width=1
Testing with valid inputs: length=1295885530, width=25191
Testing with valid inputs: length=14538, width=1
Testing with valid inputs: length=14538, width=15503
Testing with valid inputs: length=7997, width=1
...
...Testing with valid inputs: length=19378, width=22512
Testing with valid inputs: length=22512, width=22512
Testing with valid inputs: length=3392, width=44
Testing with valid inputs: length=44, width=44
.
============================================ 1 passed in 0.10s =============================================
默认情况下,Hypothesis 会对函数执行 100 次不同输入的测试。开发者可以通过使用 settings 装饰器来增加或减少测试次数。例如:
from hypothesis import given, strategies as st,settings ... ... @given( length=st.integers(min_value=1), width=st.integers(min_value=1) ) @settings(max_examples=3) def test_rectangle_area_with_valid_inputs(length, width): ... ...输出
(hyp-env) (base) tom@tpr-desktop:~/hypothesis_project$ pytest -s test_my_geometry.py
=========================================== test session starts ============================================
platform linux -- Python 3.11.10, pytest-8.4.0, pluggy-1.6.0
rootdir: /home/tom/hypothesis_project
plugins: hypothesis-6.135.9, anyio-4.9.0
collected 1 itemtest_my_geometry.py
Testing with valid inputs: length=1, width=1
Testing with valid inputs: length=1870, width=5773964720159522347
Testing with valid inputs: length=61, width=25429
.
============================================ 1 passed in 0.06s =============================================
接下来,探讨一个经典的属性:序列化和反序列化应该是可逆的。简而言之,decode(encode(X)) 应该返回 X。
我们将编写一个函数,它接受一个字典并将其编码为 URL 查询字符串。
在 hyp-project 文件夹中创建一个名为 my_encoders.py 的文件。
# my_encoders.py import urllib.parsedef encode_dict_to_querystring(data: dict) -> str:
# 这里存在一个 bug:它不能很好地处理嵌套结构
return urllib.parse.urlencode(data)
def decode_querystring_to_dict(qs: str) -> dict:
return dict(urllib.parse.parse_qsl(qs))
这是两个基本函数。它们会出什么问题呢?现在,在 test_encoders.py 中测试它们:
# test_encoders.pyfrom hypothesis import given, strategies as st
生成带有简单文本键和值的字典的策略
simple_dict_strategy = st.dictionaries(keys=st.text(), values=st.text())
@given(data=simple_dict_strategy)
def test_querystring_roundtrip(data):
"""属性:解码一个已编码的字典应该得到原始字典。"""
encoded = encode_dict_to_querystring(data)
decoded = decode_querystring_to_dict(encoded)# 需要注意类型:parse_qsl 返回的是字符串值 # 所以将原始值转换为字符串以进行公平比较 original_as_str = {k: str(v) for k, v in data.items()} assert decoded == original_as_str
现在,可以运行测试了。
(hyp-env) (base) tom@tpr-desktop:~/hypothesis_project$ pytest -s test_encoders.py =========================================== test session starts ============================================ platform linux -- Python 3.11.10, pytest-8.4.0, pluggy-1.6.0 rootdir: /home/tom/hypothesis_project plugins: hypothesis-6.135.9, anyio-4.9.0 collected 1 itemtest_encoders.py F
================================================= FAILURES =================================================
_______________________________________ test_for_nesting_limitation ________________________________________@given(data=st.recursive(> # Base case: A flat dictionary of text keys and simple values (text or integers).
^^^
st.dictionaries(st.text(), st.integers() | st.text()),
# Recursive step: Allow values to be dictionaries themselves.
lambda children: st.dictionaries(st.text(), children)
))test_encoders.py:7:
data = {'': {}}
@given(data=st.recursive( # Base case: A flat dictionary of text keys and simple values (text or integers). st.dictionaries(st.text(), st.integers() | st.text()), # Recursive step: Allow values to be dictionaries themselves. lambda children: st.dictionaries(st.text(), children) )) def test_for_nesting_limitation(data): """ 此测试断言解码后的数据结构与原始数据结构匹配。 它将失败,因为 urlencode 会扁平化嵌套结构。 """ encoded = encode_dict_to_querystring(data) decoded = decode_querystring_to_dict(encoded) # 这是一个刻意简化的断言。对于嵌套字典,它将失败, # 因为 `decoded` 版本将包含一个字符串化的内部字典, # 而 `data` 版本将包含一个真正的内部字典。 # 这就是我们揭示 bug 的方式。> assert decoded == data
E AssertionError: assert {'': '{}'} == {'': {}}
E
E Differing items:
E {'': '{}'} != {'': {}}
E Use -v to get more diff
E Falsifying example: test_for_nesting_limitation(
E data={'': {}},
E )
test_encoders.py:24: AssertionError
========================================= short test summary info ==========================================
FAILED test_encoders.py::test_for_nesting_limitation - AssertionError: assert {'': '{}'} == {'': {}}
好的,这出乎意料。现在来解读一下这个测试出了什么问题。简而言之,这个测试表明编码/解码函数对嵌套字典处理不正确。
test_for_nesting_limitation(
data={'': {}},
)
AssertionError: assert {'': '{}'} == {'': {}}
这是问题的核心。进入测试的原始数据是 {'': {}}。而从函数中解码出来的结果是 {'': '{}'}。这表明对于键 '',值是不同的:
decoded 中,值是字符串 '{}'。data 中,值是字典 {}。字符串不等于字典,因此断言 assert decoded == data 的结果为 False,测试失败。
encode_dict_to_querystring 函数使用了 urllib.parse.urlencode。当 urlencode 遇到一个字典类型的值(例如 {})时,它不知道如何处理,因此只是简单地将其转换为字符串表示形式('{}')。
关于值原始类型(它是一个字典)的信息永久丢失了。
当 decode_querystring_to_dict 函数读回数据时,它正确地将值解码为字符串 '{}'。它无法得知该值最初是一个字典。
解决方案很简单:
{ 或 [ 开头),则将其解析回 Python 对象。@given 装饰器将变得更复杂。简单来说,它告诉 Hypothesis 生成的字典可以包含其他字典作为值,从而允许任意深度的嵌套数据结构。例如:{'name': 'Alice', 'city': 'London'}{'user': {'id': '123', 'name': 'Tom'}}{'config': {'database': {'host': 'localhost'}}}下面是修复后的代码。
# test_encoders.pyfrom my_encoders import encode_dict_to_querystring, decode_querystring_to_dict
from hypothesis import given, strategies as st=========================================================================
测试 1:此测试证明嵌套逻辑是正确的。
它使用只生成字符串的策略,因此无需担心类型转换。此测试将通过。
=========================================================================
@given(data=st.recursive(
st.dictionaries(st.text(), st.text()),
lambda children: st.dictionaries(st.text(), children)
))
def test_roundtrip_preserves_nested_structure(data):
"""属性:编码/解码的往返过程应保留嵌套结构。"""
encoded = encode_dict_to_querystring(data)
decoded = decode_querystring_to_dict(encoded)
assert decoded == data=========================================================================
测试 2:此测试证明对于简单的扁平字典,类型转换逻辑是正确的。此测试也将通过。
=========================================================================
@given(data=st.dictionaries(st.text(), st.integers() | st.text()))
def test_roundtrip_stringifies_simple_values(data):
"""
属性:往返过程应将简单值(如整数)转换为字符串。
"""
encoded = encode_dict_to_querystring(data)
decoded = decode_querystring_to_dict(encoded)# 创建预期模型:一个带有字符串化值的字典。 expected_data = {k: str(v) for k, v in data.items()} assert decoded == expected_data
现在,如果重新运行测试,将得到以下结果:
(hyp-env) (base) tom@tpr-desktop:~/hypothesis_project$ pytest =========================================== test session starts ============================================ platform linux -- Python 3.11.10, pytest-8.4.0, pluggy-1.6.0 rootdir: /home/tom/hypothesis_project plugins: hypothesis-6.135.9, anyio-4.9.0 collected 1 itemtest_encoders.py . [100%]
============================================ 1 passed in 0.16s =============================================
这里所经历的,是一个经典的案例,生动展示了使用 Hypothesis 进行测试的巨大价值。原本以为两个简单且无错误的函数,实际却并非如此。
许多实际函数并不仅仅接受简单的字典;它们需要像 Pydantic 模型这样的结构化对象。Hypothesis 同样可以为这些自定义类型构建生成策略。
在 my_models.py 中定义一个模型。
# my_models.py from pydantic import BaseModel, Field from typing import List
class Product(BaseModel):
id: int = Field(gt=0)
name: str = Field(min_length=1)
tags: List[str]
def calculate_shipping_cost(product: Product, weight_kg: float) -> float:
# 一个有 bug 的运费计算器
cost = 10.0 + (weight_kg * 1.5)
if "fragile" in product.tags:
cost *= 1.5 # 易碎品的额外费用
if weight_kg > 10:
cost += 20 # 重型物品的附加费
# Bug: 如果成本是负数怎么办?
return cost
现在,在 test_shipping.py 中,将构建一个策略来生成 Product 实例,并测试这个有缺陷的函数。
# test_shipping.py from my_models import Product, calculate_shipping_cost from hypothesis import given, strategies as st为 Product 模型构建策略
product_strategy = st.builds(
Product,
id=st.integers(min_value=1),
name=st.text(min_size=1),
tags=st.lists(st.sampled_from(["electronics", "books", "fragile", "clothing"]))
)
@given(
product=product_strategy,
weight_kg=st.floats(min_value=-10, max_value=100, allow_nan=False, allow_infinity=False)
)
def test_shipping_cost_is_always_positive(product, weight_kg):
"""属性:运费不应为负数。"""
cost = calculate_shipping_cost(product, weight_kg)
assert cost >= 0
测试输出结果如何?
(hyp-env) (base) tom@tpr-desktop:~/hypothesis_project$ pytest -s test_shipping.py ========================================================= test session starts ========================================================== platform linux -- Python 3.11.10, pytest-8.4.0, pluggy-1.6.0 rootdir: /home/tom/hypothesis_project plugins: hypothesis-6.135.9, anyio-4.9.0 collected 1 itemtest_shipping.py F
=============================================================== FAILURES ===============================================================
________________________________________________ test_shipping_cost_is_always_positive _________________________________________________@given(> product=product_strategy,
^^^
weight_kg=st.floats(min_value=-10, max_value=100, allow_nan=False, allow_infinity=False)
)test_shipping.py:13:
product = Product(id=1, name='0', tags=[]), weight_kg = -7.0
@given( product=product_strategy, weight_kg=st.floats(min_value=-10, max_value=100, allow_nan=False, allow_infinity=False) ) def test_shipping_cost_is_always_positive(product, weight_kg): """属性:运费不应为负数。""" cost = calculate_shipping_cost(product, weight_kg)> assert cost >= 0
E assert -0.5 >= 0
E Falsifying example: test_shipping_cost_is_always_positive(
E product=Product(id=1, name='0', tags=[]),
E weight_kg=-7.0,
E )
test_shipping.py:19: AssertionError
======================================================= short test summary info ========================================================
FAILED test_shipping.py::test_shipping_cost_is_always_positive - assert -0.5 >= 0
========================================================== 1 failed in 0.12s ===========================================================
当使用 pytest 运行此测试时,Hypothesis 会迅速找到一个反例:一个具有负数 weight_kg 的产品可能导致负数的运费。这可能是一个开发者未曾考虑到的边界情况,但 Hypothesis 自动发现了它。
Hypothesis 不仅可以测试纯函数。它还可以通过生成一系列方法调用来测试具有内部状态的类,以尝试发现其潜在问题。现在,将测试一个简单的自定义 LimitedCache 类。
my_cache.py
# my_cache.py class LimitedCache: def __init__(self, capacity: int): if capacity <= 0: raise ValueError("容量必须是正数") self._cache = {} self._capacity = capacity # Bug: 这可能应该是一个 deque 或有序字典,以便正确实现 LRU self._keys_in_order = []def put(self, key, value): if key not in self._cache and len(self._cache) >= self._capacity: # 驱逐最旧的项 key_to_evict = self._keys_in_order.pop(0) del self._cache[key_to_evict] if key not in self._keys_in_order: self._keys_in_order.append(key) self._cache[key] = value def get(self, key): return self._cache.get(key) @property def size(self): return len(self._cache)
这个缓存的驱逐策略存在几个潜在的错误。现在,将使用 Hypothesis 基于规则的状态机来测试它。这种状态机旨在通过生成随机的方法调用序列来测试具有内部状态的对象,以识别仅在特定交互后才出现的错误。
创建文件 test_cache.py。
from hypothesis import strategies as st from hypothesis.stateful import RuleBasedStateMachine, rule, precondition from my_cache import LimitedCacheclass CacheMachine(RuleBasedStateMachine):
def init(self):
super().init()
self.cache = LimitedCache(capacity=3)# 此规则添加 3 个初始项以填满缓存 @rule( k1=st.just('a'), k2=st.just('b'), k3=st.just('c'), v1=st.integers(), v2=st.integers(), v3=st.integers() ) def fill_cache(self, k1, v1, k2, v2, k3, v3): self.cache.put(k1, v1) self.cache.put(k2, v2) self.cache.put(k3, v3) # 此规则只能在缓存被填满后运行。 # 它测试 LRU 与 FIFO 的核心逻辑。 @precondition(lambda self: self.cache.size == 3) @rule() def test_update_behavior(self): """ 属性:更新最旧的项 ('a') 应该使其成为最新的项, 因此下一次驱逐应该移除次旧的项 ('b')。 我们有 bug 的 FIFO 缓存无论如何都会错误地移除 'a'。 """ # 此时,keys_in_order 为 ['a', 'b', 'c']。 # 'a' 是最旧的。 # 通过更新再次“使用” 'a'。在一个正确的 LRU 缓存中, # 这将使 'a' 成为最近使用的项。 self.cache.put('a', 999) # 现在,添加一个新键,这将强制发生驱逐。 self.cache.put('d', 4) # 一个正确的 LRU 缓存会驱逐 'b'。 # 我们有 bug 的 FIFO 缓存会驱逐 'a'。 # 此断言检查 'a' 的状态。 # 在我们有 bug 的缓存中,get('a') 将为 None,因此这将失败。 assert self.cache.get('a') is not None, "项 'a' 被错误地驱逐了"这告诉 pytest 运行状态机测试
TestCache = CacheMachine.TestCase
Hypothesis 将生成一系列冗长的 put 和 get 操作序列。它会迅速识别出导致缓存大小超出其容量的 put 操作序列,或者其驱逐行为与我们模型不同的情况,从而揭示实现中的错误。
(hyp-env) (base) tom@tpr-desktop:~/hypothesis_project$ pytest -s test_cache.py ========================================================= test session starts ========================================================== platform linux -- Python 3.11.10, pytest-8.4.0, pluggy-1.6.0 rootdir: /home/tom/hypothesis_project plugins: hypothesis-6.135.9, anyio-4.9.0 collected 1 itemtest_cache.py F
=============================================================== FAILURES ===============================================================
__________________________________________________________ TestCache.runTest ___________________________________________________________self = <hypothesis.stateful.CacheMachine.TestCase testMethod=runTest>
def runTest(self):> run_state_machine_as_test(cls, settings=self.settings)
../hyp-env/lib/python3.11/site-packages/hypothesis/stateful.py:476:
../hyp-env/lib/python3.11/site-packages/hypothesis/stateful.py:258: in run_state_machine_as_test
state_machine_test(state_machine_factory)
../hyp-env/lib/python3.11/site-packages/hypothesis/stateful.py:115: in run_state_machine
@given(st.data())
^^^^^^^
self = CacheMachine({})
@precondition(lambda self: self.cache.size == 3) @rule() def test_update_behavior(self): """ 属性:更新最旧的项 ('a') 应该使其成为最新的项, 因此下一次驱逐应该移除次旧的项 ('b')。 我们有 bug 的 FIFO 缓存无论如何都会错误地移除 'a'。 """ # 此时,keys_in_order 为 ['a', 'b', 'c']。 # 'a' 是最旧的。 # 通过更新再次“使用” 'a'。在一个正确的 LRU 缓存中, # 这将使 'a' 成为最近使用的项。 self.cache.put('a', 999) # 现在,添加一个新键,这将强制发生驱逐。 self.cache.put('d', 4) # 一个正确的 LRU 缓存会驱逐 'b'。 # 我们有 bug 的 FIFO 缓存会驱逐 'a'。 # 此断言检查 'a' 的状态。 # 在我们有 bug 的缓存中,get('a') 将为 None,因此这将失败。> assert self.cache.get('a') is not None, "项 'a' 被错误地驱逐了"
E AssertionError: Item 'a' was incorrectly evicted
E assert None is not None
E + where None = get('a')
E + where get = <my_cache.LimitedCache object at 0x7f0debd1da90>.get
E + where <my_cache.LimitedCache object at 0x7f0debd1da90> = CacheMachine({}).cache
E Falsifying example:
E state = CacheMachine()
E state.fill_cache(k1='a', k2='b', k3='c', v1=0, v2=0, v3=0)
E state.test_update_behavior()
E state.teardown()
test_cache.py:44: AssertionError
======================================================= short test summary info ========================================================
FAILED test_cache.py::TestCache::runTest - AssertionError: Item 'a' was incorrectly evicted
========================================================== 1 failed in 0.20s ===========================================================
上述输出突显了代码中的一个错误。简单来说,这个输出表明缓存不是一个真正的“最近最少使用”(LRU)缓存。它存在以下显著缺陷:
当更新缓存中已有的项时,缓存未能记住该项现在是“最新”的。它仍然将其视为最旧的,因此该项会被过早地从缓存中逐出。
最后一个示例将探讨一种典型情况。开发者经常会编写一些函数,旨在替换旧的、较慢但功能完全正确的函数。新函数对于相同的输入必须产生与旧函数相同的输出。Hypothesis 可以极大简化在这方面的测试工作。
假设有一个简单的函数 sum_list_simple,以及一个带有 bug 的新版“优化”函数 sum_list_fast。
my_sums.py
# my_sums.py def sum_list_simple(data: list[int]) -> int: # 这是我们简单、正确的参考实现 return sum(data)
def sum_list_fast(data: list[int]) -> int:
# 一个带有 bug 的新版“快速”实现(例如,大数时的整数溢出)
# 或者,在这个例子中,一个简单的错误。
total = 0
for x in data:
# Bug: 这里应该是 +=
total = x
return total
test_sums.py
# test_sums.py from my_sums import sum_list_simple, sum_list_fast from hypothesis import given, strategies as st
@given(st.lists(st.integers()))
def test_fast_sum_matches_simple_sum(data):
"""
属性:新的快速函数的结果应始终与简单的参考函数的结果匹配。
"""
assert sum_list_fast(data) == sum_list_simple(data)
Hypothesis 会迅速发现,对于任何包含多个元素的列表,新函数都会失败。现在来验证一下。
(hyp-env) (base) tom@tpr-desktop:~/hypothesis_project$ pytest -s test_my_sums.py =========================================== test session starts ============================================ platform linux -- Python 3.11.10, pytest-8.4.0, pluggy-1.6.0 rootdir: /home/tom/hypothesis_project plugins: hypothesis-6.135.9, anyio-4.9.0 collected 1 itemtest_my_sums.py F
================================================= FAILURES =================================================
_____________________________________ test_fast_sum_matches_simple_sum _____________________________________@given(st.lists(st.integers()))> def test_fast_sum_matches_simple_sum(data):
^^^test_my_sums.py:6:
data = [1, 0]
@given(st.lists(st.integers())) def test_fast_sum_matches_simple_sum(data): """ 属性:新的快速函数的结果应始终与简单的参考函数的结果匹配。 """> assert sum_list_fast(data) == sum_list_simple(data)
E assert 0 == 1
E + where 0 = sum_list_fast([1, 0])
E + and 1 = sum_list_simple([1, 0])
E Falsifying example: test_fast_sum_matches_simple_sum(
E data=[1, 0],
E )
test_my_sums.py:11: AssertionError
========================================= short test summary info ==========================================
FAILED test_my_sums.py::test_fast_sum_matches_simple_sum - assert 0 == 1
============================================ 1 failed in 0.17s =============================================
因此,测试失败了,因为“快速”求和函数对于输入列表 [1, 0] 给出了错误的答案(0),而“简单”求和函数提供的正确答案是 1。现在既然已经知道了问题所在,就可以着手修复它。
本文深入探讨了使用 Hypothesis 进行基于属性的测试,超越了简单的示例,展示了如何将其应用于实际的测试挑战。通过定义代码的不变量,可以发现传统测试可能遗漏的细微错误。本文学习了如何:
将基于属性的测试添加到开发者的工具集中,并不会完全取代现有的所有测试。然而,它将极大地增强现有测试,促使开发者更清晰地思考代码的契约,并对代码的正确性拥有更高程度的信心。鼓励开发者从代码库中选择一个函数或类,思考其基本属性,然后让 Hypothesis 尽力去证明这些属性的错误。通过这一过程,开发者将成为更优秀的程序员。
本文仅触及了 Hypothesis 在测试方面能力的冰山一角。欲了解更多信息,请参阅其官方文档,可通过以下链接获取。
免费获取企业 AI 成熟度诊断报告,发现转型机会
关注公众号

扫码关注,获取最新 AI 资讯
3 步完成企业诊断,获取专属转型建议
已有 200+ 企业完成诊断