Search code examples
pythonunit-testingtddpython-unittest

TDD in Python - should we test helper functions?


A bit of a theoretical question that comes up with Python, since we can access almost anything we want even if it is underscored to sign as something "private".

def main_function():
    _helper_function_()
    ...
    _other_helper_function()

Doing it with TDD, you follow the Red-Green-Refactor cycle. A test looks like this now:

def test_main_function_for_something_only_helper_function_does():
    # tedious setup
    ...
    main_function()

    assert something

The problem is that my main_function had so much setup steps that I've decided to test the helper functions for those specific cases:

from main_package import _helper_function

def test_helper_function_works_for_this_specific_input():
    # no tedious setup
    ...
    _helper_function_(some_input)

    assert helper function does exactly something I expect

But this seems to be a bad practice. Should I even "know" about any inner/helper functions?

I refactored the main function to be more readable by moving out parts into these helper functions. So I've rewritten tests to actually test these smaller parts and created another test that the main function indeed calls them. This also seems counter-productive.

On the other hand I dislike the idea of a lot of lingering inner/helper functions with no dedicated unit tests to them, only happy path-like ones for the main function. I guess if I covered the original function before the refactoring, my old tests would be just as good enough.

Also if the main function breaks this would mean many additional tests for the helper ones are breaking too.

What is the better practice to follow?


Solution

  • The problem is that my main_function had so much setup steps that I've decided to test the helper functions for those specific cases

    Excellent, that's exactly what's supposed to happen (the tests "driving" you to decompose the whole into smaller pieces that are easier to test).

    Should I even "know" about any inner/helper functions?

    Tradeoffs.

    Yes, part of the point of modules is that they afford information hiding, allowing you to later change how the code does something without impacting clients, including test clients.

    But also there are benefits to testing the internal modules directly; test design becomes simpler, with less coupling to irrelevant details. Fewer tests are coupled to each decision, which means that the blast radius is smaller when you need to change one of them.

    My usual thinking goes like this: I should know that there are testable inner modules, and I can know that an outer module behaves like it is coupled to an inner module, but I shouldn't necessarily know that the outer module is coupled to the inner module.

    assert X.result(A,B) == Y.sort([C,D,E])
    

    If you squint at this, you'll see that it implies that X.result and Y.sort have some common requirement today, but it doesn't necessarily promise that X.result calls Y.sort.

    So I've rewritten tests to actually test these smaller parts and created another test that the main function indeed calls them. This also seems counter-productive.

    A works, and B works, and C works, and now here you are writing a test for f(A,B,C).... yeah, things go sideways.

    The desired outcome of TDD is "Clean code that works" (Jeffries); and the truth of things is that you can get clean code that works without writing every test in the world.

    Tests are most important in code where faults are most probable - straight line code where we are just wiring things together doesn't benefit nearly as much from the red-green-refactor cycle as code that has a lot of conditionals and branching.

    There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies

    For sections of code that are "so simple that there are obviously no deficiencies", a suite of automated programmer tests is not a great investment. Get two people to perform a manual review, and sign off on it.