Some time ago, I ranted about how everyone hates on testing private methods. To this day, I still hold the position that it’s okay in the right circumstance. However, in the wrong circumstance—such as in a codebase using design by contract—private method testing is a bad idea. Let’s talk about it!
Table of Contents
A couple years ago I was discouraged by the amount of negative opinions on testing private methods. As a result, I went on a quest to argue for the other side. Given the way that software works in the wild, I just didn’t see the value in holding to such a strict standard.
As the years have past, I’m still inclined to agree with my original take: it’s not a great idea to test private methods, but there are surely edge cases where it could be useful. In other words, it’s okay to test private methods.
Recently, however, I was teaching that same course that I trained for all those years ago when I had an epiphany: there might actually be a case where testing public methods only is the way to go. That case occurs when you strictly abide by design by contract.
Design By Contract
In my experience in software development, design by contract is pretty rare. As a result, you might not even be familiar with this concept, so I’ll quickly explain.
The core idea behind design by contract is that we design our software in a way where the public API provides a contract between client and implementer. In this case, the implementer promises that a method will behave as expected as long as the client follows the API rules.
At face value, almost all software works this way—at least in theory. What makes design by contract different is that the public API will typically lay out a contract for each method: if the client follows the method precondition, the client will get the expected postcondition.
To make sense of design by contract, it’s usually helpful to discuss an example. For instance, let’s imagine that the implementer writes a method for factorial where the input is some integer and the output is the factorial of that integer:
def factorial(base: int): """ Computes the factorial of same base integer. :param base: an integer representing some value, x, in the expression x! :return: the factorial of base """ # Some code that computes factorial
To make this code conform to design by contract, we should specify the contract. There are several ways to do this, but I’m going to stick to the doc comments:
def factorial(base: int): """ Computes the factorial of same base integer. :param base: an integer representing some value, x, in the expression x! :return: the factorial of base :precondition: 0 <= base <= 12 :postcondition: factorial = base! """ # Some code that computes factorial
Notice how both the precondition and postcondition are boolean expressions. This allows the implementer to explicitly state the exact requirements for using the method. In this case, base has to be some number between 0 and 12, inclusive. If this precondition is followed, the client will receive a proper factorial. Otherwise, anything could happen.
Connecting Design by Contract to Testing
One of the perks of following design by contract in an API is that both the client and implementer are rewarded. The client is promised some behavior without having to understand how any of the software works. Meanwhile, the implementer has full control of the underlying implementation. Both of these benefits combine when we start talking about testing.
When we strictly define the contracts of our public API (and promise to never change them), we allow ourselves as implementers to create fictional clients through testing. Each unit test then becomes a fictional client that is attempting to use our software. If we’re careful in how we craft our fictional clients, we can build out a robust test suite that has no concern for the underlying implementation details. Suddenly, we can completely change the underlying architecture, test the API, and be fairly certain we’re providing the same expected behavior to our clients.
That’s not to say that testing the underlying implementation details is a bad idea. Rather, the benefit of these private method tests are outweighed by the robust test suite we build out for the public API. As a result, while I don’t see any problems with testing private methods in general, they definitely have reduced utility in an API managed by design by contract.
Building Out a Software Discipline
All of this discussion around testing is somewhat missing the point I’m trying to convey, which is that software is really only as strong as the discipline you use when developing it. If you have no consistent set of rules to constrain your development, you end up with software that becomes difficult to maintain. Instead, if you allow yourself to be constrained by some discipline, you provide yourself a mechanism and rationale for all of your decision making. Design by contract just so happens to be one of those disciplines.
One other trick you can try for further strengthening your API is to implement some form of public method purity. The idea being that public methods will never be dependent on each other, so changes to some of the API has no effect on other methods in the API. Combining a trick like this with design by contract further breathes flexibility in the underlying implementation, despite the constraints you set for yourself.
If you’re wondering where I learned these ideas, look no further than the material in Ohio State’s computer science and engineering program. While I can’t say that I agree with everything we teach, I have grown to appreciate the idea of discipline in software development. Perhaps some day we’ll be guiding a generation of software developers through different schools of discipline like characters in a Shonen anime. Until then, we should all keep learning and growing.
Anyway, sorry for the clickbait title! I suppose I really just wanted to connect a few ideas I’ve been thinking about. As always, I rarely hold such strong opinions in tech. I find dogma to be a huge pain. With that said, let’s call it for the day. Unless you want to keep reading, in which case, here are a few related articles:
- The 28 Best Programming Languages of All Time
- Improve Code Readability by Using Parameter Modes
- Who Gets to Decide What Is and Isn’t a Programming Language?
Until next time! Take care.
Life has given me a bit of a beating, so I'm taking some time to recover. See y'all again soon.
Why Is Adding Two Random Numbers Not the Same as Generating One in the Same Range?
Generating random numbers might seem easy at first, but there are definitely some pitfalls.