Python Unit Testing – Best Practices for Testing Smaller Functions

pythonunit testing

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?

Best Answer

Don't try make such decisions up-front - it is way easier to decide when growing your unit tests alongside with the development of your functions.

You wrote

let us act like each line is a long computation so that we want to break down func into several functions

but usually that is not what you start with. func starts as a small-to-medium sized function, with some unit tests. When it grows over time, you add more tests for it, and when it comes to the point where you want to refactor parts of it into smaller functions, you already have unit tests for func in place at that time.

If not, it is time to add a few before you this refactoring, to make sure it will not break anything. At this point in time, you can have only unit tests for func, since _func_a, _func_b, _func_c don't even exist yet!

After the refactoring, _func_a/b/c will initially still be "private" functions, implementation details of `func, which means unit testing them is still not a good idea - yet.

So the obvious followup question is, when does start to make sense writing unit tests for _func_a alone? And the answer now should be almost obvious as well:

  • as soon as _func_a becomes a public function, something one can use on its own, or

  • as soon as you want to evolve and test _func_a on its own, because testing it only through func makes it hard to establish a test you trust, maybe because it becomes hard to get all the input combinations, edge cases and failure modes right.

Sometimes, the development of func does not follow this top down approach. You may decide to work more in a bottom-up process, where you first develop _func_a as a building brick before introducing it into func (maybe even before func exists). Then it should be clear that you need unit tests for _func_a first. Still it is a good idea to write also unit tests for func (maybe it is more an integration test, but how you call it does not really matter).

The other followup question here is, at which point in time does it start to make sense to mock out _func_a/b/c and test func in isolation? My short answer here is "almost never" - at least when we only talk about pure functions. I would actually start to mock out a function like _func_a when it starts hindering me to test func by a good unit test on its own. For example, when _func_a makes database or network calls, or when _func_a makes a really complex, CPU intensive calculation, then it is time to mock it out.

In the end, this is a judgement call. Drawing the line between "implementation details" and stable building bricks in your code requires some experience. It definitely cannot be judged sensibly by looking at contrived function names like func or _func_a/b_c, one has to evaluate the real situation and make an informed decision.