There is a common misunderstanding in the software world — simply writing tests is test driven development. Test driven development (TDD) is about ensuring that your software is functioning, as well as ensuring that the software’s internals are well designed, reusable, and decoupled.
What is TDD?
- You are not allowed to write any production code unless it is to make a failing unit test pass.
- You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
- You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
To summarize Uncle Bob’s rules:
- Only write code that is tested.
- Start your tests small, then work your way up.
- Only write enough production code to make a test pass.
The basic process of TDD has 3 steps: Red, Green, and Refactor.
The red step consists of writing a failing test, only one failing test. If you keep getting ahead of yourself and writing multiple tests, borrow an idea from Getting Things Done and get the ideas out of your head so they don’t get in the way of other thoughts. There are a number of different mechanisms you can use: create a to-do list on paper, make an index card, create a series of TODO comments in your files, etc. I find the physical action of crossing off an item on paper or writing a check mark and folding an index card in half gives me more of a sense of accomplishment.
Your first test for a new object should be simple. TDD focuses on emergent design, the opposite of big design up front. Let the design fall out of your code. The purpose of the first test is not about functionality. It’s about flushing out the usage of what you are about to create.
Start with the inputs: “What do I have to feed into this function?” Next, think about the outputs: “What will this function be spitting out?” Then write an assertion, run your test suite, and verify that the test you just wrote is red.
All the other test cases in the red stage should be about capturing functionality. Don’t just test the happy path; think about the craziest thing you could do with the function or object. What happens when I pass in a null parameter? What happens when I pass in a negative value? How about when I pass in a string when it’s expecting an integer?
The green step consist of making the failing test pass as quickly as possible. If more than one test is failing, start with making the test you just wrote pass, and then continue working the reds to greens one at a time. Don’t worry about how the code looks or how efficient it is. Your concern should be with making the test pass so you can move on to ensuring the next bit of functionality is under test.
The next step is refactoring, restructuring, and organizing your code. The refactoring step can occur at anytime — after 1 red/green cycle, after 4 red/green cycles, etc. Since you have a number of passing green tests, you can refactor with ease and comfort, knowing that your tests will fail if you regress and lose functionality.
Refactoring shouldn’t only be about restructuring your code and making it more easily readable. Tests need refactoring love and attention too, but don’t refactor code and tests at the same time.
Benefits of TDD
One of the primary benefits of TDD is that you have functioning and working code at all times. You spend time narrowing in on pieces of functionality and ensuring that they work as intended.
TDD allows for fearless changes. I worked on a number of software projects prior to being enlightened by the magic of TDD. A common thread of thought, looking back, is that I was always deathly afraid of making changes. I spent more time using the application than I did writing code, just to make sure I was maintaining functionality and not causing regressions in specific features. With TDD, that fear is removed because functionality is under test, and you’re able to get near-instantaneous feedback about the system or parts of it. The ability to make fearless changes via refactoring causes the internal quality of your software to improve and eventually bleeds through to being external quality.
TDD also provides you with a living documentation of the code. If you are anything like me, then when exploring a new library, you want to skip all the fluffy documentation and cut to the chase, looking at examples on how to use it. It is important that we keep this in mind when writing and refactoring our tests — it’s our responsibility to make the test readable and easily understandable. Unlike comments or extremely long manuals, tests are executable and will tell you if they are lying.
Designing Through Code
I am always troubled that one of the D’s in TDD doesn’t stand for design. As I touched on briefly before, the practice of TDD is not entirely about writing tests, ensuring coverage and working software. TDD flips software development on its head. It forces you to think about the problem from the outside in, instead of from the inside out.
Writing tests first forces you to not be worried or concerned about implementation, the primary worry and concern is with using your object or function. Since we are spending a fair amount of time directly interacting with the objects and functions we are writing, architecture and design come to the forefront.
When Not to TDD
What if you’re using a new library or framework and you don’t know how to use it? How can you write tests first if you don’t know how to begin? The answer is you can’t. Create a new project, and use the new library away from your production code base. This new project is known as a spike. Since this isn’t production code, you aren’t violating Rule #1 of Uncle’s Bob’s 3 basic rules. Code until you feel comfortable with the library. When you know how your library works, ignore the spike, go back to your production code and start writing tests.
However, just because you can’t TDD, do not completely throw away the discipline of writing tests. Your tests will serve as a reference for you and (if your spike is in source control) a reference for those who come behind you. Using these tests, you can quickly recall what you have learned, and you will hopefully be able to look back and see a progression in your technique.