我有一个函数链在我的库,看起来像这样,从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%会引发您期望的异常。
在代码中,即使在应该失败的时候没有引发异常,测试仍然会通过。你可以手动修改,但是使用框架工具会使代码更容易阅读和更简洁。