The continuing dissent and confusion about unit testing of private class methods surprises me.
The access specifier is much like your choice of software license: it exists to limit consumers’ actions, not to limit yours. A method’s access specifier is completely irrelevant to testing, and only describes what you want the consumer to use; any code that takes inputs and produces outputs, private or not, should be tested.
The opponents of private-method testing tend to argue in quasi-religious terms: that private methods are mere hidden implementation details; that users of the class will only care about the public API; that testing of private methods breaks encapsulation. A typical unhelpful “solution”: private methods should be put into a different class and made public there.
To argue against granular testing of private methods is to mean well while being thoroughly unhelpful. The purpose of testing is more than just to guarantee the viability of your public interface – it is also to examine the inner machinery and support routines of your class to ensure that they themselves function correctly for a spectrum of inputs and edge cases. The private implementation will contain non-trivial complexities that are more readily and precisely tested directly than via the public API.
The usual counter-argument says that testing the public API inherently covers the private API. This is not wrong – and of course you absolutely should test your public API – but stopping there is like having half a pie instead of a whole. You’ve built the house without knowing the bricks are good. And in game and simulation programming I have repeatedly found that testing “the bricks” is particularly critical; most of these support routines are highly mathematical in nature, after all. This is where you confirm that your vector operations behave like you’d expect, that your fuel calculation is right, that your steering behaviors work for the edge cases, your AI planner handles all the different inputs you can throw at it, etc. You build confidence in the whole by building confidence in the parts.
After you make the philosophical choice to test private methods you will discover myriad benefits to your code in terms of style, readability and stability. Here’s how it will work:
- You will now feel encouraged to refactor calculations and routines into private methods with dependency injection.
- These refactorings will partition great, monolithic methods into easily understood functions with clear signatures and supporting documentation.
- The massive, monolithic methods you once had will now be reduced substantially in size, suddenly becoming much more readable.
- You’ll be able to reason about your code. This newfound cognitive understandability will expose weaknesses and edge cases for you to address, thereby leading to more robust code.
- And oh, by the way: you will write more and better tests because you can now mentally model the small things you have extracted.
Always remember: bugs love to hide in complexity. When we factor complexity out into small, single-purpose, easily read, easily tested chunks…we stamp out bugs. It is virtually inevitable. And like perfect little individual bricks being laid into a strong and sturdy wall, small robust chunks layer into large robust codebases.
The philosophy isn’t without its drawbacks. First, because you are testing small, highly specific routines instead of (only) the broad API, your tests may prove more brittle as you change your code. To emphasize: this is okay. Tests exist to support the code – not the other way around. If code modifications down the road alter your invariants and invalidate your tests, do what you would always do: change the tests to match the new worldview! (You’ll inevitably find that your public-facing API tests remain sturdy – as they should – while your private tests are the more brittle ones, but that’s no reason to throw away private testing.)
Second, some languages make it hard to test private methods. In C++ there are hacks to do so; “#define private public” is one thuggish way, but a far more elegant version is to use friend status to gain access. I particularly like (and use) the mechanism described by “jdm” in this Stack Overflow answer. It works beautifully, is mostly noninvasive, and keeps the code clean and organized.
Test your private methods. The only argument not to is fundamentally a religious one, and you and your code will be better off for your pragmatic approach.