I genuinely love unit testing and try to evangelize it as much as possible, be it by blogging, public speaking, or in whatever way I can.
This wasn’t always the case, though. When I first learned about the concept of unit testing, I was highly skeptical about the whole thing. I couldn’t understand how writing these so-called tests could bring any value to my application. I thought it was just a massive waste of time.
But with time, I started to see the value of unit testing, this practice that, at first, seemed absurd to me. What changed my mind? In a word, experience. When I started to write tests, I realized that many, if not all, of the preconceived notions I had about the technique, didn’t hold up to the scrutiny.
In today’s post, I’ll make a public mea culpa. These are all opinions I once had about unit testing. They are also all widespread objections people have to this practice. If you’re trying to sell someone (perhaps your manager?) the idea of unit testing, those are all some points you might have to counterargue.
So, without further ado, here are some confessions of a former unit testing skeptic.
Confession #1: I Thought Unit Tests Didn’t Test Anything
When I first started learning about unit testing, I struggled to understand what value they brought to the table. I’d look at a unit test and fail to grasp what was being tested. In my defense, I’m far from being the only one who struggled with this. On the contrary, this is a typical reaction I get from beginners whenever I teach unit testing.
This all might sound a little too abstract, so let’s see a concrete example. Consider the following excerpt:
I’d see a test case like this one and scratch my head. My thought process was roughly like this: “OK, so I create this test and say that the number 5 should be converted to ‘V.’ But what prevents me from saying that it should be converted to, let’s say, ‘banana,’ and then changing the code in the NumberConverter class to make it so?”
For those of you experienced with unit testing, my reasoning back then will sound somewhat silly. On the other hand, in case you’re not that knowledgeable about it, it’s possible that you agree with past me right now.
Here’s the thing: it’s all too common for beginners to misunderstand the role unit testing plays in the overall quality strategy of an application. Sure, this thing is awesome, but it’s no replacement for other types of testing and for additional tools and techniques you can use to assure quality.
Unit testing beginners often also fail to understand that a single test case, on its own, is not that useful. The value comes when you have many test cases, comprehensively covering your app’s behavior.
So going back to the question from my less-experienced but definitely skinnier former self with better eye-sight, “What’s preventing me from doing an obviously wrong implementation but writing a test case validating it?”
And the answer is, obviously, nothing. But hopefully, there are other barriers your code must pass before it gets to production. For starters, it probably won’t pass code review. If it passes, other forms of testing—such as integration, acceptance, end-to-end, and even manual exploratory testing—should detect the error.
Unit testing is no silver bullet. What it is instead is a vital component of a healthy quality assurance strategy.
Confession #2: I Thought Writing Unit Tests Made Development Slower
One of the most persistent misconceptions about unit tests is the belief that writing them slows down development. This just won’t die. And the likely reason is that it sounds like it’s true. I mean, you spend n hours implementing a feature without unit testing. It’s only logical that you’d spend more than n hours implementing the same feature this time with tests. Right?
Well, I certainly thought like that. That’s no longer true, though. Here’s why: when you write software using unit testing, you spend less time debugging. This is especially true if you employ TDD. But even when writing the tests after the production code, you’ll see this effect in action.
Writing unit tests will make you introduce fewer defects in the code. If you develop code in small amounts, which are always covered by unit tests that you run constantly, then you’re in a very good situation. If you make a mistake, the tests will warn you. Then you won’t have to debug for hours to find its source. All it takes is reverting the small amount of code you’ve produced since the last successful execution of the tests. Throwing away a few minutes worth of work doing this hurts nothing compared to, let’s say, throwing away five hours.
And of course, the same applies when using TDD, just in an opposite order:
- You write a simple, failing test.
- Then you write the simplest possible code that will make the test pass.
- After the test passes, you can refactor the code in order to remove duplication and make it cleaner overall.
If you write a test that you thought should fail and it passes, there’s something wrong. Either the test is wrong somehow, or you’ve just exposed some flaw in the production code.
On the other hand, if you’ve just finished some production code that should make a test pass, but it still fails…that’s bad news as well. Take a step back and investigate. Maybe the production code isn’t quite right. The opposite is also possible, and maybe it’s the test wasn’t correct to begin with.
I’m sure you’ve got the picture. By employing a development workflow that makes use of unit tests to receive immediate and constant feedback from the code, you spend less time on the debugger. So yeah, of course, writing tests will add to the total development time. But that’s an investment that pays off handsomely in the future.
Confession #3: I Thought Writing Unit Tests Was Hard
I can already imagine the indignation of some readers right now: “What do you mean? Writing tests is hard!” To those of you vehemently agreeing with my former self, I say,” yep, sort of.” Here’s the thing. Writing tests should be easy, but sometimes, it’s hard. Other times, it’s incredibly hard.
Even though it’s often hard to add tests to a given piece of code, that’s not to blame on unit testing, but rather the code itself.
I know it sounds completely Boromir-ish to say that, one does not simply add unit tests to any piece of code. The code has to be testable in order to be…well, tested. And this is probably the biggest-known secret of unit testing, and it’s a fact that many beginners take a long time to realize, understand, and put into practice. I know I did.
If you want your code to be testable, you have to explicitly do so. You have to write it in a certain style, so to speak. A style that, by employing a series of principles and techniques, such as dependency injection, can give birth to code that is less complex, has low coupling and is highly maintainable.
Code that is hard to test reveals a flaw in the code, not in the technique of unit testing. In a certain way, we could say that the unit testing is criticizing your design, offering a very valuable feedback.
I’ve Seen the Light…About Unit Testing
No, unit testing is not a panacea. It’s possible to do it wrong. You’ll face difficulties. But it’s definitely 100% worth it. Starting to unit test is, hands down, the #1 thing you should do for your code (and for your career as a developer).
I had held some very wrong notions about unit testing in the past. It took me a lot of studying and hard work, but I got rid of them. I’ve finally seen the light and I’m not going back to the dark ages of coding without tests. Are you?
This post was written by Carlos Schults. Carlos is a .NET software developer with experience in both desktop and web development, and he’s now trying his hand at mobile. He has a passion for writing clean and concise code, and he’s interested in practices that help you improve app health, such as code review, automated testing, and continuous build.
Carlos Schults is a .NET software developer with experience in both desktop and web development, and he’s now trying his hand at mobile. He has a passion for writing clean and concise code, and he’s interested in practices that help you improve app health, such as code review, automated testing, and continuous build.