2

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?

MattSom
  • 2,097
  • 5
  • 31
  • 53
  • 1
    Does this answer your question? https://stackoverflow.com/a/68508868/126014 – Mark Seemann Aug 04 '21 at 13:14
  • 1
    Most of what I would've said is in [@VoiceOfUnreason's answer](https://stackoverflow.com/a/68651638/1431750). What I'd add is: (1.) It's good that you're refactoring code. (2.) If the helper functions are marked private with the leading underscore - you don't need to test it beyond the scope of its existing usage - but it definitely needs to be tested. (3.) The unit tests you write for the helpers serve as documentation for their capabilities and limitations. This is especially important when other large/main functions start using them. – aneroid Aug 04 '21 at 13:25
  • @MarkSeemann Thanks for the link, this answer shows the same sentiment I was getting from my problem. – MattSom Aug 04 '21 at 13:45
  • @aneroid I agree, in my cases these helpers were exclusively used by this one function. This case I think it is acceptable to rely on the original tests. Other way they clearly should be tested since those are more like utility functions at that point. – MattSom Aug 04 '21 at 13:45

2 Answers2

3

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.

VoiceOfUnreason
  • 52,766
  • 5
  • 49
  • 91
  • 1
    Thanks for the interesting insight on your view! I remained calling only my main function for now, since my inner functions are actually just helpers (long input checkers, etc) and thus are not viable outside of this main function scope. – MattSom Aug 04 '21 at 13:19
0

Too many private/helper functions are often a sign of missing abstraction.

May be you should consider applying the 'Extract class' refactoring. This refactoring will solve your confusion, as the private members will end up becoming public members of the extracted class.

Please not, I am not suggesting here to create a class for every private member but rather to play with the model a bit to find a better design.

Pankaj
  • 302
  • 4
  • 7