I've a chain of functions in my library that looks like this, from 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
Due to the randomness in func_a
, the output of fun_c
is non-deterministic. So sometimes when you do:
>>> 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
Then I've modified func_c
to catch the error and explain to users why ZeroDivisionError
occurs, i.e.
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
And the expected behavior that raises a ZeroDivisionError
now shows:
---------------------------------------------------------------------------
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!
I could test the func_c
in a deterministic way to avoid the zero-division without iterating func_c
multiple times and I've tried:
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}
And when I need to test the new exception, I don't want to arbitrarily iterate func_c
such that I hit the exception, instead I want to mock the outputs from func_a
directly to return the 0 value.
func_c
?I've tried this in my testfuncs.py
file in the same directory as myfuncs.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")
First of all - yes, to test non-deterministic function you should ideally mock all values that result in different scenarios. Packages like hypothesis can help you with that.
Few things that I noticed and would recommend to change
testfuncs.py
file to the same directory. It's good to have all tests under different directory (usually tests) so you have easier controll over which tests to run or can prepare a deployment package without tests.class BadLuckException(Exception):
def __init__(self):
super().__init__("You've lucked out")
# test_som.py
with pytest.raises(BadLuckException):
func_c(12)