如何在非确定性函数的模拟单元测试中测试异常?



我有一个函数链在我的库,看起来像这样,从myfuncs.py

import copy
import random
def func_a(x, population=[0, 0, 123, 456, 789]):
sum_x = 0
for _ in range(x):
pick = random.choice(population)
if pick == 0: # Reset the sum.
sum_x = 0
else:
sum_x += pick
return {'input': sum_x}
def func_b(y):
sum_x = func_a(y)['input']
scale_x = sum_x * 1_00_000
return {'a_input': sum_x, 'input': scale_x}

def func_c(z):
bz = func_b(z)
scale_x = bz['b_input'] = copy.deepcopy(bz['input'])
bz['input']  = scale_x / (scale_x *2)**2
return bz

由于func_a的随机性,fun_c的输出是不确定的。所以有时候当你这样做的时候:

>>> func_c(12)
{'a_input': 1578, 'input': 1.5842839036755386e-09, 'b_input': 157800000}
>>> func_c(12)
{'a_input': 1947, 'input': 1.2840267077555213e-09, 'b_input': 194700000}
>>> func_c(12)
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-121-dd3380e1c5ac> in <module>
----> 1 func_c(12)
<ipython-input-119-cc87d58b0001> in func_c(z)
21     bz = func_b(z)
22     scale_x = bz['b_input'] = copy.deepcopy(bz['input'])
---> 23     bz['input']  = scale_x / (scale_x *2)**2
24     return bz
ZeroDivisionError: division by zero

然后我修改了func_c以捕获错误并向用户解释ZeroDivisionError发生的原因,即

def func_c(z):
bz = func_b(z)
scale_x = bz['b_input'] = copy.deepcopy(bz['input'])
try:
bz['input']  = scale_x / (scale_x *2)**2
except ZeroDivisionError as e:
raise Exception("You've lucked out, the pick from func_a gave you 0!")
return bz

引发ZeroDivisionError的预期行为现在显示:

---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-123-4082b946f151> in func_c(z)
23     try:
---> 24         bz['input']  = scale_x / (scale_x *2)**2
25     except ZeroDivisionError as e:
ZeroDivisionError: division by zero
During handling of the above exception, another exception occurred:
Exception                                 Traceback (most recent call last)
<ipython-input-124-dd3380e1c5ac> in <module>
----> 1 func_c(12)
<ipython-input-123-4082b946f151> in func_c(z)
24         bz['input']  = scale_x / (scale_x *2)**2
25     except ZeroDivisionError as e:
---> 26         raise Exception("You've lucked out, the pick from func_a gave you 0!")
27     return bz
Exception: You've lucked out, the pick from func_a gave you 0!

我可以在不迭代func_c多次的情况下以确定性的方式测试func_c以避免零除,我已经尝试过:

from mock import patch
from myfuncs import func_c
with patch("myfuncs.func_a", return_value={"input": 345}):
assert func_c(12) == {'a_input': 345, 'input': 7.246376811594203e-09, 'b_input': 34500000}

当我需要测试新的异常时,我不想任意迭代func_c,这样我就会遇到异常,相反,我想直接模拟func_a的输出以返回0值。

Q:如何让模拟捕获新的异常而不通过func_c多次迭代?

我在myfuncs.py的同一目录下的testfuncs.py文件中尝试了这个:

from mock import patch
from myfuncs import func_c
with patch("myfuncs.func_a", return_value={"input": 0}):
try:
func_c(12)
except Exception as e:
assert str(e).startswith("You've lucked out")
我是如何检查错误消息内容的正确方式来检查异常在模拟测试?

首先—是的,要测试非确定性函数,理想情况下应该模拟导致不同场景的所有值。像hypothesis这样的包可以帮助你。

我注意到并建议修改

  • 你写你添加testfuncs.py文件到同一目录。最好将所有测试放在不同的目录下(通常是tests),这样您就可以更容易地控制要运行哪些测试,或者可以准备一个没有测试的部署包。
  • 虽然我在一些地方发现了检查日志消息的做法,但我建议不要这样做。我们不希望在更改消息中的某些错别字后出现测试失败的情况。在你的情况下,你可以创建自己的异常,并检查它是否被引发
class BadLuckException(Exception):
def __init__(self):
super().__init__("You've lucked out")
# test_som.py
with pytest.raises(BadLuckException):
func_c(12)

我看你的还行。单元测试通常分解并单独测试每个组件,并运行所有可能的执行路径。从本质上讲,您希望确保每行代码至少运行一次以发现任何问题(而不是在生产环境中运行时才发现问题)。

也就是说,在各种测试套件中有一些工具可以帮助处理常见的测试用例。一个是pytest.raises()或unittest的assertRaisesRegex,它断言该上下文中的代码100%会引发您期望的异常。

在代码中,即使在应该失败的时候没有引发异常,测试仍然会通过。你可以手动修改,但是使用框架工具会使代码更容易阅读和更简洁。

最新更新