Having a hard time sorting through the vast array of books, talks, blog posts, contradictory advice, and academic writing about Test-Driven Development?
I’ve spent the last quarter wading through resources on TDD, and I’ve summarized the most important things any new TDD developer should know into these six areas.
1. What is TDD?
Test-driven development is, quite simply, software development that is driven by the writing of tests. The simplest way to describe this method is using the “red-green-refactor” method.
Any time you are about to add a feature to your code base, the first thing you do is write a test for that functionality, even though there is currently no code written to accomplish the task being tested. Write the test, run it, and watch it fail, or go red.
Once you’ve verified that the test fails, write the minimal amount of code needed to make that test pass, or turn green. Now that you have a passing test, refactor the code you’ve written to follow clean coding standards, running the test again to make sure you’ve preserved functionality.
After you’ve refactored and ensured the test still passes, pick a new feature to work on and write a failing test for that.
2. Why Should I Write Tests?
According to Steve Freeman in his book, Growing Object-Oriented Software, Guided by Tests, one of the main goals of writing tests is to give yourself confidence in your code.
Kent Beck also mentioned this several times in “Is TDD Dead?”, a series of talks between Beck, Martin Fowler, and David Heinemeier Hansson. Beck states that the reason tests are so valuable is that they provide developers with instant feedback, telling them whether a change they just made to the codebase preserved the current functionality.
You can see this benefit in the refactor step of “red-green-refactor.” If, in attempting to clean up the code you wrote to get the test to pass, you accidentally break the implementation, running the test and seeing it fail will immediately tell you so.
This concept can be expanded to larger refactors. Let’s say you have an application that you are scaling up, and in doing so, you want to refactor a class that has become too bloated, moving the functionality into several smaller, more modular classes. Having a test suite in place and knowing that those tests will catch regressions gives you the confidence you need to implement that massive change.
While you refactor the class, you can regularly run the test suite to see what functionality you’ve successfully re-implemented and what still needs to be updated. Once the entire suite is green again, you know you’ve succeeded.
3. How Does Testing Drive My Development?
At first glance, the concept of TDD does seem a bit backward. Write the test first? Why should I write a test for code that doesn’t even exist yet? It seems like a big waste of time.
But one of the biggest values you can gain from TDD is that writing the test actually helps you realize what you want the code you’re going to write to look like. In writing the test, you outline how you want to interact with this new object or function from the outside, what its interface should be, and what kind of values you’d expect it to return. This all gets fleshed out while you’re writing the test. Once you have the test written and failing, it’s much easier to jump in and write the actual implementation because you know exactly what you want it to do.
4. What Should My Tests Look Like?
That depends on what you’re testing. There are different kinds of testing. Unit tests test the functionality of individual classes or modules in isolation from the rest of the code. Integration tests ensure that pieces of code work together correctly. End-to-end tests confirm that data entered in one end of the application is output from the other end as expected, testing that every piece of the app works together in the expected way.
There are hundreds of testing frameworks for various languages and many different approaches to test functionality at these different levels. It’s impossible to give advice that is applicable to tests 100% of the time, but these guidelines will generally be helpful.
Only test one thing at a time.
You want your test failures to help you immediately hone in on the problematic code, and that can be challenging if you have a test that wants build an object, test all of the functions that object can perform, and then test that the object can be destroyed cleanly.
It is better to break out each aspect that you want to verify into its own test. That way, when one test fails, you know exactly what is causing the problem, and you can immediately start fixing it.
Make sure your test is readable.
Like any clean code, your tests should be self-documenting. But because this code will only be run in isolation, not ever in production, you can take steps to be more explicit rather than more efficient. One easy way to achieve this is to be explicit in naming your tests to describe what they’re testing.
TestCalc() is an efficient test name, but at a glance, it tells you nothing about what is being tested. Are you testing a calculation? A calculator? Some calculus method? Does this test verify that the function returns a value? Or manipulates it? A better test name would be something like this
Another way to document your test is with the
It blocks in Ruby’s RSpec framework.
5. Does This Mean I Always Need to Write Tests?
This was the biggest point of contention in the “Is TDD Dead?” discussion. David Heinemeier Hansson claimed that TDD was dead because it was not useful in every single instance of development. He actually pointed out several instances in his own career where following “red-green-refactor” had been detrimental to his codebase. Beck and Fowler replied to this concept that TDD is only one tool in the developer’s toolbox. It should never be the end-all, be-all of development.
Let’s say you’re creating a new parameter on an existing object. This parameter needs a getter and setter for the value. Should you TDD that? I would say no. There is no real value provided by ensuring that calling
getName() returns the value of the object’s
Name parameter. If that simple of a function breaks, then you might have a problem with the entire coding framework you’re using, and a test might not help you fix that.
6. So How Do I Know When I Should Write Tests?
There are a few ways to determine if you should write a test for a functionality you’re about to implement.
First, you can always ask yourself, “What value does this test provide me? What will I learn when it fails?” If the answer is nothing, then you probably don’t need to write that test.
You can also ask if writing the test will help clarify the functionality you’re about to implement. If yes, then writing the test will undoubtedly be valuable.
When in doubt, write the test. Doing so will give you additional experience writing tests, provide you with all the benefits of practicing TDD, and if you get to the end and decide that the test doesn’t provide value, you can just remove the test. This whole process will have helped you grow as a test-driven developer!
Additionally, don’t be afraid to ask! Talk to the senior dev on your project, or just ask another developer whose testing skills you admire. Get their thoughts on what you should and shouldn’t test. If you’re pairing and your partner is writing a test, ask them to talk you through their thought process for writing the test. Listening to more experienced developers is one of the fastest ways to grow your own testing intuition.
Here are a few of the resources I found most helpful while honing my knowledge on TDD.
- The RSpec book: Behaviour-Driven Development with RSpec, Cucumber, and Friends by David Chelimsky, Dave Astels, Zach Dennis, Aslak Hellesøy, Bryan Helmkamp, Dan North
- Growing Object-Oriented Software, Guided by Tests by Steve Freeman and Nat Pryce
- “Is TDD Dead?”, a series of conversations between Martin Fowler, Kent Beck, and David Heinemeier Hansson