Most people don’t get TDD the first (or second, or third, or fourth) time they’re introduced to the concept. It’s not a topic that is taught in university and it’s not practiced in most development teams. It’s an altogether foreign idea which goes against our sensibilities, prompting us to ask questions like:
Even Kent Beck, TDD’s “re-inventor”, didn’t get it at first:
Clearly TDD isn’t a concept which seems reasonable to developers at first, nor is it a technique which comes naturally to us. Or is it?
At its simplest, Test Drive Development (TDD) is a technique used to build software by first writing tests describing what the code should do and then writing only the code necessary to make the tests pass.
Martin Fowler describes the process like this:
- Write a test for the next bit of functionality you want to add.
- Write the functional code until the test passes.
- Refactor both new and old code to make it well structured.
—Martin Fowler TestDrivenDevelopment
More can be said about the process (and has), but that is the core of TDD.
The “Non-TDD” development process is what most of us begin using from day one. There is nothing formal about it and there’s definitely nothing automated. There is a problem and we create a solution for that problem according to our understanding at the time. If you were to codify the process, it would look like this:
Maybe we could add more steps to the process, but this is what most developers begin using in their careers, and what most continue to use.
If you look closely at the two processes—and maybe squint a little—you’ll see that they’re nearly identical.
|Development Step||TDD||Not TDD|
|Determine Results||Create test to prove results||Expect specific outcomes|
|Write Code||Complete when tests pass||Complete when results meet expectations|
|Cleanup||Refactor new and existing code||Refactor new code. Fix any noticed errors in existing code|
While each technique goes about the steps differently, they are the same steps. If there is any hard difference between the two techniques, it’s that one stores the expectations for future use. And it’s that difference that makes all the difference.
Just look at the advantages that come with storing your expectations (i.e. tests) for future use:
And those are just the advantages that come with storing a collection of tests. Let’s look at the benefits that come with the intentional practice of TDD.
TDD is no silver bullet. Many articles would have you believe that by practicing TDD, your code will magically become clean, you’ll intuitively practice SOLID principles, and you will work harder. It doesn’t work like that. TDD is just a technique. While adhering to the technique makes some bad practices more difficult, it doesn’t make them impossible, and it certainly won’t turn you into a “rock star” developer.
What you will find by practicing TDD is that you are forced to really think through the API of your code. When you write tests first, you are attempting to use the code before it’s even written, and by doing so you are asking, “How would the interface work best?” As you answer that question, you’ll find it’s easiest to answer with simple methods and functions. Simpler code is easier to test.
Not only will you find it easier to write simpler code, but you will also write less code. Too often we, as developers, write code against what might be needed. We’re not trying to over-engineer the code, we’re just trying to anticipate. Unfortunately, this often leads to excessive, unused, or even unnecessary code. Code which must now be maintained.
By first specifying the expectations with tests, we are constrained to code against those expectations. There is no opportunity to over-engineer the code, because the expectations—real or imagined—don’t exist.
Not only do the tests force you to think through the interface of your API and write code specifically to address those needs, they also document the behavior of the API. Look at this example:
describe "User#name" do it "returns first and last names joined with a space" do contact = Contact.new(first_name: "User", last_name: Example") contact.name.must_equal "User Example" end it "returns the first name if the last name is blank" do contact = Contact.new(first_name: "User") contact.name.must_equal "User" end it "returns the last name if the first name is blank" do contact = Contact.new(last_name: "Example") contact.name.must_equal "Example" end end
You don’t need to know Ruby to read the code above. The first line tells us that we are looking at the instance method
name. Each “it” line, tells us what
name is supposed to do in the given scenarios. We don’t have to read the code, we can read the actual requirements in our language of choice.
Developers notoriously hate writing documentation. Even when we do write it, we usually fail to maintain it. As they say, “Wrong documentation is worse than no documentation.” Writing tests provides an almost effortless way of documenting our code.
Let’s look at some hard numbers. In a Microsoft funded research paper, authors Nagappan, Maximilien, Bhat, and Williams discovered that TDD resulted in an increase of 15-30% in initial development time, but a 40-90% decrease in defects. As one author noticed:
It is interesting to note that the figure of 15-30% longer during the coding phase. Then, the testers found 40-90% fewer bugs. That’s 40-90% fewer bugs that need fixing. Now, these bugs that need fixing were found in the functional testing phase. Exact figures will vary, but it is frequently observed that bugs found here will take at least 10 times longer to fix than had they been found during development.
–– John Ferguson Smart For A Fistful of Dollars
Furthermore, Matt Hawley collated the benefits and results of several research papers in his article TDD Research Findings and provided the following list:
These findings are great if you are making a case to management to allow you and your team to start practicing TDD, or even if you’re just participating in an internet debate. But the real benefit of TDD is summarized by “Uncle” Bob Martin in his answer to a question on Quora:
If you follow TDD, and use it to build a test suite that you trust. And if that test suite executes in seconds (a design goal). Then you will not be afraid of the code. You will not be afraid to clean it. You will not be afraid to fix it. You won’t be afraid to do anything to the code, because your tests will tell you if you’ve broken it.
—Uncle Bob Martin (https://www.quora.com/What-are-the-benefits-of-TDD)
For the sake of argument, let’s pretend that what I’ve written has persuaded you to take that first step into Test Driven Development. Where do you begin? And what can you do to make that transition smoother?
Because everyone has their own opinions about how testing should be done, multiple testing frameworks—each with their own API—are available for every language. While having this variety is great, most TDD adherents recommend learning the testing library that comes with your language. The reason for this is this are four-fold:
Like most things, when you first start using TDD, you’ll be tempted to start using it everywhere, even on code you’re not currently working on. Although this isn’t necessarily a bad thing, remember that you still have work to perform and the existing code works. Instead, focus on adding tests to the code you’re actively working with.
As new features are requested, or as bugs are found, you’ll eventually touch more and more of the system. As you do, your test coverage will slowly expand to cover everything.
Your first tests are going to be rubbish. They’ll tests too many things in one go; You’ll tests things that don’t need to be tested; You’ll touch parts of the system that shouldn’t be touched; and you’ll just make it harder on yourself than you need to. That’s part of the process. You don’t know where boundaries are or how to do things the “right” way and your initial tests will be you figuring that out.
That’s okay. That’s part of learning. Just be prepared to laugh at six-months-ago-you and replace those initial tests with what you’ve learned.
Not everything in TDD is as easy as
assert User.name() == "Joe Bagadonuts".
Sometimes figuring out how to test something can be really hard, like
interacting with the network or even just
stdio. Give it a good try to figure
out how to test something, but don’t get trapped. You can always come back to
add tests later when you have a better understanding.
Learning how to test isn’t easy. You’ll make mistakes, test the wrong things, make things too complicated, and even question the value of testing in general. Be patient with yourself and the process. It takes time. Learning always does. You’ll get the hang of it and you’ll see the rest of your code improve as you do.
Eventually, as your test suite grows, one of those tests is going to catch something and everything’s going to click. For me, it was when I came to a dead end using a particular library. I knew the best choice was to switch it out for something else, and it’s only because I had a suite of tests that I had the confidence to do so. That’s when everything gelled.
As we saw at the beginning of the article, TDD and the “traditional” development techniques both follow the same steps to meet their goals, but differ in what they do with their expectations. With TDD, expectations are stored in the project and become a suite of tests developers can use and refer to to ensure the code continues to meet the expectations. With the less structured approach, however, expectations are ephemeral, being stored in the developer’s head, or in one-off tests, only disappear into the void or be overwritten when the work is “done”.
But the work is never “done”, and the customers’ hunger for more features is never satisfied. Every developer eventually needs to revisit and make changes to code which was considered “done”. Will you be able to confidently make changes to that code, knowing that you have a suite of tests to support you, or will you need to recreate the steps necessary to ensure it works? Will your tests tell you what breaks when you make changes, or will your customers?
At first blush, it may seem like TDD is only piling work onto your plate. After all, you need to learn a new API, new programming practices, and change your mindset. But as your test suite expands and more and more bugs are caught you will find yourself becoming more productive than ever, and you’ll come to wonder how you ever managed to release code to production before.