2

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?
1

2 Answers 2

2

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.

3

I'm sure someone will provide you with a more detailed answer, but here are some things to consider:

Unit tests are used to verify that the behavior of a function is correct and to prevent regressions so that you can make changes to implementation details without affecting that behavior and therefore not requiring a change to the tests.

The obvious way to write them would be to test the public interface of the system under test, in this case the functions as a black box. This means you would test for various kinds of inputs that the outputs are correct. Do this for all inputs, and you are done :)

In your specific example, you could write these tests for func_{a,b,c} and also func. However, I would consider your proposal to test that arguments are passed in the correct order an implementation detail. Just test func in the same way: Given an input, verify the output (especially for pure functions).

Sometimes this black-box approach is not enough. In your case, you have dependencies (hard coded, in the form of func_{a,b,c} within your main function and want to make sure that they are called (more relevant if you're dealing with state, e.g. persisting stuff to a database, other side-effects). This is where it becomes tricky: What is an implementation detail that should not be checked, and what should be checked? I dont believe there is a general answer, but would consider:

  • How likely is the implementation going to change?
  • How much effort is a change in the implementation causing for changing the unit tests and mocks?
  • Could this scenario better be handled by integration/end-to-end tests?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.