What Really Matters in Test-Driven Development
It isn't about the tests -- it's about the feedback.
- By Peter Provost
Recently someone asked me to provide guidance to someone wanting to convince development teams to adopt Test-Driven Development (TDD). I wrote a rather long reply that was the seed leading to this article. My answer was simple: You can't convince people to do TDD. In fact, you shouldn't even try, because it focuses on the wrong thing.
TDD is a technique, not an objective. What really matters to you as a developer is that your code does what you expect it to. You want to be able to quickly implement your code, and know that it does what you want. By focusing on this, you can more easily see how you can use testing techniques to achieve your goals.
All developers do some kind of testing when they write code. Even those who claim not to write tests actually do. Many of you simply create little command-line programs to validate expected outcomes. Or you might create special modes for your app that allow you to confirm different behaviors.
What TDD Is (and Isn't)
Unlike ad hoc methods similar to those described here, TDD defines a regimented approach to writing code. The essence of it is to define and write a small test for what you want -- before you write the code that implements it. Then you make the test pass, refactor as appropriate and repeat. You can view TDD as a kind of scientific method for code, where you create a hypothesis (what you expect), define your experiment (the tests) and then run the experiment on your subject (the code).
TDD proponents assign additional benefits to this technique. One of the most important is that it strongly encourages a highly cohesive, loosely coupled design. Because the test defines the interface and how dependencies are provided, the resulting code typically ends up easy to isolate and with a single purpose. Object-oriented design defines high cohesion and loose coupling to be essential properties of well-designed components.
In addition, TDD provides a scaffold for when you're unsure about how to implement something. It allows you to have a conversation with the code, asking it questions, getting answers and then adjusting. This makes it a great exploratory tool for understanding something new.
TDD is not a panacea, however. While the tests produced can serve to protect from regressions, they aren't sufficient on their own to assess and ensure quality. Integration tests that combine several units together are essential to establish a complete picture of quality. End-to-end and exploratory testing will still be required to evaluate the fitness of the entire system.
In short, TDD is another tool that you can use while writing code. It has benefits -- but it has costs, as well. It can help you define components with very little up-front definition, but it will add to the time required to create the code. It has a strong, positive impact on the design of your code, but it requires practice and experience to learn, and can be frustrating for the novice.
When you think about TDD as a tool, you should recognize that, like all tools, it has times when it's effective and times when it isn't. As my father used to tell me, "You can put in a screw with a hammer, but it probably isn't the best choice."
There are times you begin with a clear picture of how a component should work. You have a clear mental model of what it should do and how to code it. Trying to baby-step into that design using a test-first technique would be tedious and time consuming. Nevertheless, the design benefits from the TDD approach are still desirable. In these cases, it would be nice if you could get the best of both worlds.
TDD leads to these design benefits primarily by forcing you into a rapid back-and-forth between consuming code (tests) and the system under test. This, I believe, is where the real benefit comes from TDD. You write a small amount of code, then a small amount of tests. Cycle back and forth between them frequently. I call this short-cycle testing.
Tests are a tool that let you ask questions of your code. By asking questions frequently, you can fine-tune what you're building. Sometimes you'll write a test first; sometimes after. Avoid staying in either mode too long. Keep the back-and-forth fluid and frequent.
Even when you know what you want, switching to a test lets you confirm your thinking. You'll often find that the test tells you something unexpected, letting you correct it earlier than you otherwise might have. The tests will also help you find dependencies and coupling you never realized were there. Trying to write tests after an extended free-coding period will also tell you these things, but by then it will be significantly harder to do anything about it.
Quick and Correct Code
I said at the beginning that developers really want to be able to quickly write the correct code and know it does what they think it does. There are a few key elements in that statement. As a developer you want to be efficient and do your work quickly. You also want to create the right code. Moreover, you need to be sure the code you write does what you think it does. This is the essential goal of developer testing and what differentiates it from other kinds of testing.
Developer tests are an effective tool that deliver quality and design benefits. TDD provides the result, but the biggest benefit comes not from dogmatic application of TDD, but from using a short-cycle mode of writing tests and code at almost the same time.
Peter Provost is a principal program manager lead at Microsoft, where he focuses on making Visual Studio the supreme development environment for enterprise and Agile customers. In his 20-plus years in the software industry he has worked as a program manager, developer, team Lead, architect and consultant. He first got started with Agile development more than 10 years ago when he read an article about Extreme Programming, and has never gone back.