In Defense of Unit Testing

Kent Rancourt
7 min readMar 19, 2019

I can’t believe I need to write this in 2019.

Hopping in the Time Machine

I wrote my first unit test sometime around 2005. I was two years out of college and two years into a thirteen year stint at a large Java shop. While I won’t make any claim to have been the first among my colleagues to write a unit test, I can say with certainty that test-driven development wasn’t a common practice there in 2005. As my role within the company changed over the next couple of years, I found myself in a position to demonstrate and advocate for broad improvements to our development methodologies. Testing was at the top of my list.

When I started evangelizing unit tests to colleagues in other departments, I was often told, “We’d love to write unit tests if we had the time.” I would counter with, “You don’t have time not to write unit tests.” As it turns out, this was a very easy thing to prove. You see — we were a WebSphere shop and starting or restarting WebSphere could take several minutes on our resource-constrained laptops. Do I really need to connect the rest of the dots? Even if we fell short of the TDD ideal (writing tests first), unit testing became a common practice company-wide over the next couple years.

I recall, sometime around 2009, casually entertaining a position at another company — a much smaller company. In the course of my interview, it came to light that they not only didn’t practice test-driven development, but they were actually averse to it. By 2009 test-driven development was so widely embraced by the Java community that I couldn’t help but recognize this as a red flag. If their organization rejected one engineering practice so widely regarded as foundational, what other corners were they cutting? My choices seemed clear in that moment: 1. allow hard-won engineering skills (it wouldn’t just be testing) to atrophy 2. lead them on their journey to TDD 3. walk away. №1 was never an option for me. They made it clear that they had no interest in №2. So №3 it was. My interviewer and myself walked away with a mutual understanding that we weren’t a good fit for one another.

I’ve never had any regrets about the outcome of that interview. But if you’d stopped me in the parking lot on my way back to my car and told me that ten years later in 2019, I’d still be defending the value of test-driven development to naysayers, I’d have been as incredulous as Pythagoras learning that 2,500 years later, some people still contend the earth is flat. But here we are.

So Why Am I Writing This?

Clearly, a recent event precipitated this post — a colleague tweeted their perspective that, essentially, unit tests are useless. To be perfectly frank, I consider this position so flimsy that I have avoided directly quoting the tweet to promote anonymity — I’m not here to embarrassment or incense anyone. I will paraphrase their argument: If developers cannot be relied upon to write good code in the first place, how can they be relied upon to write good test cases? Now, one may ask, if I am going to such lengths to preserve my colleague’s anonymity, why even respond at all? One simple reason — flat earthers. My colleague’s illogical point of view received more than what I am able to consider an acceptable number of likes and retweets. My inescapable conclusion is that there are people out there, including many who may never have even seen that tweet, who need to hear the other side of this. It’s 2019 and I still need to explain why unit tests are beneficial.

“If developers cannot be relied upon to write good code in the first place, how can they be relied upon to write good test cases?”

Parsing My Colleague’s Argument

Let’s unpack my colleague’s question. “If developers cannot be relied upon to write good code in the first place,” initially suggested to me that the argument deals strictly with the quality of the people behind the code. Stated more explicitly: Bad developers cannot be trusted to test their own code.

So what about good developers? Should they write tests? For the moment, let’s assume so.

I’m actually uncomfortable talking about “good developers” or “bad developers,” in large part because I don’t know how to objectively differentiate between the two. For sure, good code and bad code both exist. We know this is true because we could write tests that objectively prove or disprove the logical correctness of a piece of code. (There may be other attributes that qualify code as good or bad, but for our immediate purposes, logical correctness is a relevant measure.) Let us then consider “good developers” to be those who have written “good code,” which is, at the very least, logically correct. Conversely, we’ll consider “bad developers” to be those who have written “bad code,” which is, at the very least, logically incorrect.

Paradoxically, without tests to objectively determine whether a given piece of code is good or bad, we cannot know whether the author of that code was a “bad developer” who is therefore unqualified to test the code or a “good developer” who is. Perhaps we can avert this paradox by amending my colleague’s argument: Bad developers cannot be trusted to test their own code; but good developers don’t need to. Since my colleague has a recent history of not testing their code and, I assume, considers themself a good developer, I believe this amended statement might be the most sensible interpretation of their tweet.

My colleague’s argument, as I’ve interpreted it, seems to render the question of whether a piece of code is good or bad completely moot — since we’re not going to test it either way. As if zero code coverage were not already bad enough, this argument against testing is literally predicated on an assertion that code quality does not matter. This makes me uncomfortable.

This argument against testing is literally predicated on an assertion that code quality does not matter.

My Counter-Argument

So far, I’ve only dissected my colleague’s argument, arriving, with some effort, at their intended meaning — and rejected that argument on the basis of its undesirable conclusions. Now it’s time for me to offer up an alternative, pro-unit-test model.

First, let’s dispense entirely with any notions of “good” or “bad,” simply because they’re not useful. There are no good or bad developers. There are only developers that have written good code or bad code. Since we cannot objectively know if code is good or bad until after it is tested, we cannot use code quality to determine a developer’s qualifications to test their code. If this forces us to treat all code the same and if we’ve already agreed that treating all code the same by not testing it is tantamount to declaring code quality unimportant, then our only option is to treat all code the same by testing all of it.

Unencumbered by the distinction between good and bad code, we can now attempt to enumerate the benefits that we hope all code receives through unit testing. This is a non-exhaustive list from the top of my head:

  1. Unit testing promotes agility by facilitating change. As software grows over time and new features are added, the likelihood that existing code will be modified or even significantly refactored is high. The existence of unit tests promotes the ability to refactor with confidence.
  2. Unit tests explicitly document the expected behaviors of the code under test. Specs are good. Executable specs are better. To be fair to my colleague, they acknowledge this, but their insistence that unit testing is otherwise useless would seem to nullify any possibility of ever realizing this benefit.
  3. Unit testing reduces costs by finding bugs earlier. Think of the most expensive bug imaginable slipping through QA— one that results in millions of users shunning your software or one that exposes you to legal action. Are you uninterested in beginning the hunt for such a bug at the earliest possible moment — i.e. when the code that introduced the bug was first written?

The true output of unit testing — which must be viewed as an iterative and ongoing process — is quality code.

Other types of tests exist. There are behavior-driven tests, integration tests, end-to-end tests. We haven’t discussed any of these. I am fond of saying that these sort of tests are useful for asserting that you’ve built the right thing, and the unit tests are useful for asserting that you’ve built the thing right. Couple this notion with the list of benefits we hope to achieve and something that I hope becomes clear is that unit testing isn’t really about assessing the quality of code — it’s about ensuring it. i.e. The output of unit testing isn’t a measure of code quality. (In fact, this is what code coverage reports are for — reporting the percentage of code that is of unknown quality.) The true output of unit testing — which must be viewed as an iterative and ongoing process — is quality code.

Anecdotally, I have never encountered a developer who unit tests their code and claims never to have prevented a grievous logical error by doing so.

--

--

Kent Rancourt

Kent is a founding engineer at Akuity, working primarily with Kubernetes, Argo CD, and other open source projects.