I’ve been thinking about test-driven development a lot lately, observing myself veering between TDD virtue and occasional lapses into sin. Here’s my thesis: As a profession, we do a lot more software maintenance than we do greenfield development. And it’s at the maintenance end where TDD really pays off. I’m starting to see lapses from the TDD credo as more and more forgivable the closer you are to the beginning of a project. And conversely, entirely abhorrent while in maintenance mode.
Other Voices · I was deeply impressed by “Uncle” Bob Martin’s RailsConf keynote (see also his follow-up exegesis), which at its core argued for “professionalism”, specifically as expressed in the deep-TDD rules:
Never write code until you have a failing test.
Never write any more code than is necessary to un-fail the test.
Also, it’s worth visiting Kent Beck’s The Open/Closed/Open Principle and especially InfoQ’s Kent Beck Suggests Skipping Testing for Very Short Term Projects, which comes with lots of third-party commentary.
Why We Do It · I think TDD makes these claims:
You build better software initially.
You remove the fear from maintenance in general and refactoring in particular.
Maybe I’m weird, but I think the second is the one that matters. After all, we do way more maintenance than initial development. And in my experience, the first-cut release of any nontrivial software is pretty well crap. But since software is after all soft, you can go back and whack it and whack it and whack it until you’ve whacked it into shape.
But to do that well, you absolutely must have enough test coverage that you just aren’t afraid to rip your code’s guts out, rearrange them, and put them back in a better configuration.
Sin and Penitence · People introducing TDD do this thing where they start from scratch saying “We’re going to write a class to represent X and it’ll need a method to do Y, so let’s write a test for Y”. The problem is, when I’m getting started, I never know what X and Y are. I always end up sketching in a few classes and then tearing them up and re-sketching, and after a few iterations I’m starting to have a feeling for X and Y.
And maybe the sketch-and-tear process would be better and more productive if I had the patience to write the tests for each successive hypothesis about X and Y, but I don’t think I ever will. This is partly because the first few tests for the kind of classes I write tend to be expensive; where you have to face the dependency-injection and big-fat-mock problems. I lack the patience to make that investment if I’m unsure the class is basically pointing in the right direction.
But here’s what I do. When I have a few X’s and Y’s that start to feel right, then I go back and fill in tests for all the methods, and I use rcov and friends to help me. And I don’t check things in till that’s done.
I freely admit that this is not really truly TDD. I hope there are no adherents of the TDD church who would consequently argue that it’s not worth doing.
Redemption · On the other hand, once you’re into maintenance mode, there are really no excuses. Because you really know what all your X’s and Y’s are, either because X is already there and you’re adding Y to meet a well-understood need, or because your previous choice of X and Y were wrong and you have a better one in mind. So write the tests first and don’t try to go any further than where they take you. Put another way, the most rewarding place to build your test code is on a foundation of existing tests.
For me, this is TDD at its most addictive, the engineering part of software engineering; where that “professionalism” thing comes into sharp focus.