A Case for Test-First Development

[article]
Summary:
You may feel you don't have time to write unit tests, but you really don't have time not to. Steve Poling makes the case that writing tests first not only will yield better code, but will help you get that code working right sooner. Here's how using a test-first approach changes your thinking about coding, lets you see mistakes immediately, and helps you create more testable code.

My employer hired a new manager a few months after I first drank the test-driven development Kool-Aid. He said he believed in writing unit tests for production code—good. I asked him whether he wrote unit tests or code first. He said code first.

I understood. There are times when the pressure to get something done makes you think that writing unit tests are a distraction. Suppose your boss walks up and asks what you're working on—you don't want to say you're coding up unit tests. (Although if you're the manager asking your loyal minions what they're working on, hope they'll say they're coding up unit tests.)

You may feel you don't have time to write unit tests, but you really don't have time not to. Here's my case that writing tests first not only will yield better code, but will help you get that code working right sooner.

Writing Tests First Changes Your Thinking

Maybe you recall your introductory programming classes, where your professor exhorted you to resist the urge to code. They said this to keep you from going off half-cocked without any thought about design or algorithms.

Thinking is better than coding. You benefit from design thinking because your thoughts are at a higher level of abstraction than when you are thinking about code. Likewise, a test-first approach adds an important aspect to your thoughts about your code.

Testing first pushes your thinking away from production code so you can see it from the outside looking in, helping you think more seriously about interfaces and your API. When you write unit tests, your code's first user is you. By writing tests first, you'll naturally make it easy on yourself as you're working out the hooks between your unit tests and your production code. With a better interface, your code will be easier to use, and that benefit will compound throughout its lifecycle.

As you think more about your code at different levels of abstraction (high-level versus low-level) and from different aspects (inside versus outside), the quality of your reasoning about your code will improve. It might even be a good idea to document your thought process about why you think certain unit tests are important so you can review it later.

With each unit test, you'll find that your reasoning about your code improves as "passes" confirm your understanding and "fails" contradict your wishful thinking. I always write code faster when I understand what I'm doing than when I'm still a little bit foggy about the details (or downright wrong in my thinking). In fact, wishful thinking and erroneous conclusions about my code slow me down worse than anything.

You may sympathize with me when I say that I've made some little change that's too simple to fail, so I don’t bother testing it. And what do you suppose happens after that? Because the fail was "impossible," I looked in every wrong place for the fix before I heeded Sherlock Holmes's words: "How often have I said to you that when you have eliminated the impossible, whatever remains, however improbable, must be the truth?" This makes for satisfying detective stories, but it's hell on your velocity.

A Test-First Approach Uncovers Errors Sooner

You may be far more accurate than I am, but I've made and fixed so many mistakes that I've noticed a pattern: Recent mistakes are easier to fix than distant past mistakes.

Fred Brooks's book The Mythical Man-Month was written in the dark ages of waterfall thinking. He observed that mistakes made at the initial analysis stages were the most expensive to repair. As we've progressed to more agile methodologies, we've compressed the time between analysis and implementation. I believe this has reduced the cost of rework more than anything.

Now, consider two scenarios. In the first scenario, you find an error at the last moment before your code ships. You fix it and look like a hero. In the second scenario, you find an error immediately after you make the mistake. This situation is less heroic, but consider the developer experience. You never have a better grasp of your own intent as in the moment you write a line of code. Over time you have to work to get your head back to that point, and the longer the time, the harder the work.

Much of what passes for "technical debt" is the accrued work the devs must spend restoring their mental context to where it was when they wrote the code in the first place. I can appreciate the need to incur technical debt to bring in a crash project on time. But it's foolish to do so in such a way that it takes you longer to release. Forgoing unit tests is a false economy that promises speed but delivers handicaps. If you must incur technical debt, choose wisely.

Testing first enables you to see mistakes immediately—when you understand your code best and can fix it most easily. This benefit swamps anything you might get from deferring tests.

Finally, when you write tests first, you create more testable code. The way you'll structure your objects and how you separate concerns will follow the intent of your unit tests. As you write good unit tests, some of the things that make code untestable will be difficult or impossible. You'll naturally gravitate toward dependency injection and separate the concerns of object creation and business logic. You'll naturally think of the seams you've designed around your code under test.

Now, suppose you've bought into a test-first approach but have never done it. The good news is that most modern development environments make it easy. Write the test for a nonexistent method and you'll see the editor put a little red squiggle beneath it. If you're lucky, your development environment will let you right-click and tell it to create the code to make the red squiggle go away. And as you add parameters to the method, you'll get more squiggles and generate more code automatically.

It's fun. It's faster. And it's the righteous way to develop software.

About the author

CMCrossroads is a TechWell community.

Through conferences, training, consulting, and online resources, TechWell helps you develop and deliver great software every day.