使用 pytest 和 hypothesis 进行异常处理和测试
Exception handling and testing with pytest and hypothesis
我正在为带有假设的统计分析编写测试。当传递非常稀疏的数据时,假设使我在代码中出现 ZeroDivisionError
。所以我调整了我的代码来处理异常;在我的例子中,这意味着记录原因并重新引发异常。
try:
val = calc(data)
except ZeroDivisionError:
logger.error(f"check data: {data}, too sparse")
raise
我需要通过调用堆栈向上传递异常,因为顶级调用者需要知道存在异常,以便它可以将错误代码传递给外部调用者(REST API 请求).
编辑:我也不能给val
赋一个合理的值;本质上我需要一个直方图,当我根据数据计算合理的 bin 宽度时会发生这种情况。显然,当数据稀疏时,这会失败。如果没有直方图,算法将无法继续进行。
现在我的问题是,在我的测试中,当我做这样的事情时:
@given(dataframe)
def test_my_calc(df):
# code that executes the above code path
hypothesis
不断生成触发 ZeroDivisionError
的失败示例,我不知道如何忽略此异常。通常我会用 pytest.mark.xfail(raises=ZeroDivisionError)
标记这样的测试,但在这里我不能这样做,因为相同的测试通过了良好的输入。
像这样的东西是理想的:
- 像往常一样继续测试大多数输入,但是
- 当出现
ZeroDivisionError
时,将其作为预期失败跳过。
我怎样才能做到这一点?我还需要在测试正文中放一个 try: ... except: ...
吗?我需要在 except 块中做什么才能将其标记为预期失败?
编辑:解决@hoefling 的评论,将失败案例分开是理想的解决方案。但不幸的是,hypothesis
没有给我足够的句柄来控制它。至多我可以控制生成数据的总数和限制(最小值、最大值)。然而,失败案例的分布非常狭窄。我无法控制它。我想这就是假设的意义所在,也许我根本不应该为此使用假设。
以下是我生成数据的方式(稍微简化):
cities = [f"city{i}" for i in range(4)]
cats = [f"cat{i}" for i in range(4)]
@st.composite
def dataframe(draw):
data_st = st.floats(min_value=0.01, max_value=50)
df = []
for city, cat in product(cities, cats):
cols = [
column("city", elements=st.just(city)),
column("category", elements=st.just(cat)),
column("metric", elements=data_st, fill=st.nothing()),
]
_df = draw(data_frames(cols, index=range_indexes(min_size=2)))
# my attempt to control the spread
assume(np.var(_df["metric"]) >= 0.01)
df += [_df]
df = pd.concat(df, axis=0).set_index(["city", "category"])
return df
from hypothesis import assume, given, strategies as st
@given(...)
def test_stuff(inputs):
try:
...
except ZeroDivisionError:
assume(False)
assume
调用将告诉 Hypothesis 这个例子是 "bad" 并且它应该尝试另一个,而不会使测试失败。如果你有这样的功能,它相当于在你的策略上调用 .filter(will_not_cause_zero_division)
。 See the docs for details.
我正在为带有假设的统计分析编写测试。当传递非常稀疏的数据时,假设使我在代码中出现 ZeroDivisionError
。所以我调整了我的代码来处理异常;在我的例子中,这意味着记录原因并重新引发异常。
try:
val = calc(data)
except ZeroDivisionError:
logger.error(f"check data: {data}, too sparse")
raise
我需要通过调用堆栈向上传递异常,因为顶级调用者需要知道存在异常,以便它可以将错误代码传递给外部调用者(REST API 请求).
编辑:我也不能给val
赋一个合理的值;本质上我需要一个直方图,当我根据数据计算合理的 bin 宽度时会发生这种情况。显然,当数据稀疏时,这会失败。如果没有直方图,算法将无法继续进行。
现在我的问题是,在我的测试中,当我做这样的事情时:
@given(dataframe)
def test_my_calc(df):
# code that executes the above code path
hypothesis
不断生成触发 ZeroDivisionError
的失败示例,我不知道如何忽略此异常。通常我会用 pytest.mark.xfail(raises=ZeroDivisionError)
标记这样的测试,但在这里我不能这样做,因为相同的测试通过了良好的输入。
像这样的东西是理想的:
- 像往常一样继续测试大多数输入,但是
- 当出现
ZeroDivisionError
时,将其作为预期失败跳过。
我怎样才能做到这一点?我还需要在测试正文中放一个 try: ... except: ...
吗?我需要在 except 块中做什么才能将其标记为预期失败?
编辑:解决@hoefling 的评论,将失败案例分开是理想的解决方案。但不幸的是,hypothesis
没有给我足够的句柄来控制它。至多我可以控制生成数据的总数和限制(最小值、最大值)。然而,失败案例的分布非常狭窄。我无法控制它。我想这就是假设的意义所在,也许我根本不应该为此使用假设。
以下是我生成数据的方式(稍微简化):
cities = [f"city{i}" for i in range(4)]
cats = [f"cat{i}" for i in range(4)]
@st.composite
def dataframe(draw):
data_st = st.floats(min_value=0.01, max_value=50)
df = []
for city, cat in product(cities, cats):
cols = [
column("city", elements=st.just(city)),
column("category", elements=st.just(cat)),
column("metric", elements=data_st, fill=st.nothing()),
]
_df = draw(data_frames(cols, index=range_indexes(min_size=2)))
# my attempt to control the spread
assume(np.var(_df["metric"]) >= 0.01)
df += [_df]
df = pd.concat(df, axis=0).set_index(["city", "category"])
return df
from hypothesis import assume, given, strategies as st
@given(...)
def test_stuff(inputs):
try:
...
except ZeroDivisionError:
assume(False)
assume
调用将告诉 Hypothesis 这个例子是 "bad" 并且它应该尝试另一个,而不会使测试失败。如果你有这样的功能,它相当于在你的策略上调用 .filter(will_not_cause_zero_division)
。 See the docs for details.