Say we have a function of the form
def func(num: int) -> int:
num = num + 1
num = 2 * num
num = num**3
return num
and let us act like each line is a long computation so that we want to break down func
into several functions. For example,
def _func_a(num: int) -> int:
return num + 1
def _func_b(num: int) -> int:
return 2 * num
def _func_c(num: int) -> int:
return num**3
def func(num: int) -> int:
num = _func_a(num)
num = _func_b(num)
num = _func_c(num)
return num
which is a common pattern when writing and testing code. However, I find that it is often difficult to find the right balance in terms of unit testing.
Some people will only write unit tests for func
. However, in my experience these are people that will write very long functions. Moreover, if we actual deal with complicated computations these unit tests will be horrible to maintain.
The next step would be to write some unit tests for each _func_{a,b,c}
. Then, we could check that func
calls _func_{a,b,c}
with something like
def test_calls_func_b(mocker):
with mocker.patch.object(main, "_func_b"):
_ = my_module.func(1)
my_module._func_b.assert_called_once()
However, one might then debate that there is still considerable room for error. For example, a mistake like
def func(num: int) -> int:
num = _func_a(num)
_func_b(num). # mistake: no assignment!
num = _func_c(num)
return num
will go unnoticed. Thus, I often find myself in the situation of writing additional tests like
def test_calls_func_b(mocker):
func_a_return_value = mocker.Mock()
with mocker.patch.object(main, "_func_b"), mocker.patch.object(
main, "_func_a", return_value=func_a_return_value):
_ = main.func(1)
main._func_b.assert_called_once_with(func_a_return_value)
that check that what comes out of _func_a
goes into _func_b
and so so on (eventually what is return form func
is the return value of _func_c
). This is what I feel like provides good safety and will catch a good amount of mistakes. The only downside is that this quickly translates in having to have quite a few mocks in a single test - in particular, if there are more functions than just the three _func_{a,b,c}
and these have several arguments each (here they only have on). Therefore, I met several people that absolutely disdain having these sort of tests and consider them overkill. Perhaps the refusal is also rooted that many people still find mocks complicated.
So my questions are:
- Is there anything that I could be improved, or written more efficiently?
- Are there any best practices for this? Any discussions in books, blog posts, guidelines?