Maybe It’s Not Okay to Test Private Methods—at Least When Using Design by Contract

Maybe It's Not Okay to Test Private Methods Featured Image

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

Shifting Positions?

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:

Until next time! Take care.

Coding Tangents (43 Articles)—Series Navigation

As a lifelong learner and aspiring teacher, I find that not all subjects carry the same weight. As a result, some topics can fall through the cracks due to time constraints or other commitments. Personally, I find these lost artifacts to be quite fun to discuss. That’s why I’ve decided to launch a whole series to do just that. Welcome to Coding Tangents, a collection of articles that tackle the edge case topics of software development.

In this series, I’ll be tackling topics that I feel many of my own students have been curious about but never really got the chance to explore. In many cases, these are subjects that I think deserve more exposure in the classroom. For instance, did you ever receive a formal explanation of access modifiers? How about package management? Version control?

In some cases, students are forced to learn these subjects on their own. Naturally, this forms a breeding ground for misconceptions which are made popular in online forums like Stack Overflow and Reddit. With this series, I’m hoping to get back to the basics where these subjects can be tackled in their entirety.

Jeremy Grifski

Jeremy grew up in a small town where he enjoyed playing soccer and video games, practicing taekwondo, and trading Pokémon cards. Once out of the nest, he pursued a Bachelors in Computer Engineering with a minor in Game Design. After college, he spent about two years writing software for a major engineering company. Then, he earned a master's in Computer Science and Engineering. Today, he pursues a PhD in Engineering Education in order to ultimately land a teaching gig. In his spare time, Jeremy enjoys spending time with his wife and kid, playing Overwatch 2, Lethal Company, and Baldur's Gate 3, reading manga, watching Penguins hockey, and traveling the world.

Recent Posts