I believe that writing code using testing[1] as a design activity yields long-term benefits that make my life easier.
Though I’m a strong believer, I’ve struggled with TDD in the past. I’ve found I get bogged down in keeping the red-green-refactor cycle going. Sometimes I have to work with code that is lacking sufficient tests, but I know I can’t boil the ocean before I proceed to whatever I’m really trying to do with the code. Other times, I’m not sure if I’m testing the right things; I could be missing tests in one place and writing too many tests in another.
Three easy pieces
Last week, I read three insightful pieces and made one discovery of my own that deepened my understanding of the practice of TDD. Allow me to share.
First off, Kent Beck has been exploring the phases in the life of a startup. The earliest stage is proving the idea. He later asserted that when you’re doing stuff like this, you can drop TDD, temporarily. I thought about this and it clicked. If you’re working on a prototype, where you’re trying to explore an idea and see if it works, you don’t want to iterate on the code, as TDD would have you do. You want to iterate on the idea. TDD will just slow you down.
On the other end of the spectrum, you’ve got maintenance programming. Once a startup, company or project has proven their idea and shipped a system, you are maintaining software. Keeping it working, living, breathing. For this sort of development, where you’re making small, focused changes without adding significant functionality, Tim Bray pointed out that TDD is critical. Using it to drive the process of fixing bugs, cleaning up the system and adding minor functionality is really handy for figuring out if you’ve broken something in some dark corner. It also helps the next guy to do the same. It’s a win, and I suspect it’s the sweet-spot of TDD.
If you imagine prototyping and maintenance as opposite ends of the software life-cycle spectrum, adding significant new features to existing software probably lies somewhere in the middle. You may need to “poke around” to decide if what you’re doing is right, like when you’re prototyping. But once you’re done, you want some way for others who have to maintain the software (such as yourself) to figure out what it’s supposed to do and whether assumptions have been broken.
Then I read an anecdote by Uncle Bob about how he worked out an ambitious new feature in Fitnesse. He and his pair got a new feature working, celebrated, and called it a night. When he woke up, he realized they weren’t actually done; they still had to clean it up, by writing tests.
Oh. Duh.
This was the missing link, for me. Sometimes, you need to iterate on the idea first. If skipping the tests helps you, so it goes. Once you’ve got the idea working (and committed!), then start writing tests[2]. Once you’re happy with the structure and coverage of your code, you commit again and then push it to your peers[3].
The crux of my revelation is this: you get the benefits of TDD-as-a-design-activity by doing it. When you do it is immaterial. You just have to do it.
My own revelation is blindingly obvious in retrospect. If the going gets tough, proceed in this order: get it to work, write some tests for it, then clean up the code. Sometimes you can break this cycle if what you’re working on doesn’t take too much cognitive capacity. But if you overflow your mental buffer, you have to break it down into steps and work through the cycle. Failing to realize this was one of the causes of me bogging down in TDD.
Context is everything. Always.
Adding context to answer the question of when you start writing tests is something I haven’t found much writing on until recently. I’m increasingly finding that considering the situation is a great ninja-move in my quest towards writing beautiful, useful code.
fn1. Call it a test, example, behavior, or story. Whatever.
fn2. Jim Weirich did a great presentation on how to backfill tests on existing code.
fn3. Pardon the Git-centric terminology[4].
fn4. If you are not already, please start using Git immediately.