I happen to be a TDD proponent. Now, right there I’ve probably lost about half of you, and the rest are probably going, “okay, yeah, get on with it.” TDD is one of those things that people either love or hate. For a full meditation on the ins and outs of technical hype, I’ll refer you to my other blog. In the meantime, if you’re one of those people whose sneer at the thought of TDD is still stuck to your face, I’ll ask you to indulge me for a few more paragraphs (two of which are only one sentence long). After that, if you still feel like TDD is the stinkiest thing to come along since Pepé Le Pew, you can either jump over to the link above, or just continue on, mentally subsituting “TDD” with some technical process you actually like. Or, you know, find something else to do entirely.
My first experience with TDD was via XP (that is, Extreme Programming, not the horrid Windows version), specifically the original book by Kent Beck, where it’s referred to as “test-first programming.” Like many of the ideas therein, test-first programming was completely insane. I knew instinctively that it could never work. So I immediately decided to try it. Why? Probably because I often find myself in the position of telling other people about insane ideas that they’re positive can’t possibly work, and hypocrisy is one of my pet peeves. So I decided to pick a project (not a paid one; one that I was working on on the side) and use test-first programming at least until I reached my first milestone. And, let me tell you: I hated it. Utterly despised it.
For about two or three weeks.
Then, all of a sudden, the lightbulb went on. Just like it had when I suffered through learning vi for the first time. Just like it did about halfway through my first project using version control (RCS, it was, back in those days). Just like it did when I finally grokked spreadsheets using Lotus 1-2-3, or (as I mention in the other blog post) OOP while learning C++. And now I use TDD a lot, and I evangelize it to my co-workers. It really has changed the way I program.
Still, I’m fond of saying I’m not a TDD zealot. TDD is good sometimes, but it’s not for every project, I say. Heck, last time I mentioned (ever so briefly) TDD in this blog, I even referred you over to chromatic’s blog, where he gives a great, balanced view on using TDD (and not using TDD). And definitely go read that, if you didn’t last time I pointed you at it: it really does say everything I might want to. You use TDD when you should, and not when you shouldn’t. And I’m still going to stand by that.
But I’m starting to rethink how how often the “shouldn’t” comes up.
See, I had an experience recently (a couple months ago, but it’s taken a while for the idea to percolate into a blog post) where I had to do a “quick” thing for work. This thing was to enable our QA department to test some stuff that, right now, is a huge PITA to test. So my code would not actually be part of our codebase; it would not “go to production” and be run by our customers. It was strictly for use by QA, and it was all fiddling with data in the database (and not even the production database), and it was just a really quick thing I was going to hack together.
And, one of the downsides of using TDD is, it takes longer. Now, why would you ever use a developement technique (and, don’t forget: TDD is not a QA technique, but rather a development technique: that’s why it’s “TDD” and not “TDQ”) that slows you down? Well, there are two big reasons why the speed hit is not as awful as it sounds.
The first is that TDD often slows you down in a good way. Like, you know how sometimes you’re just ripping along, coding like a bat out of hell, and you realize you’ve been coding in completely the wrong direction? for several hours? Yeah, TDD eliminates a lot (but not all!) of that. When you have to write the tests first, it essentially forces you to design the interface first, and not just from a theoretical perspective: you have to write working (well, non-working, at first) code that actually uses your stuff. That helps settle your thinking and avoids a lot of (but not all!) blind alleys.
Secondly, you will produce software with fewer bugs when using TDD. This is not something that can be definitively proven, but case studies so far bear it out, and, if you’ve tried it both ways, you’ll know it’s true. Does the time saved fixing bugs exceed the time lost due to slower development? Probably ... almost certainly. Does it make up for any lost opportunity costs in being slower to market? Ah, that’s much trickier ... I’m not aware of any studies that even attempt to address that thorny issue. But it’s often the yardstick I use when deciding to TDD or not to TDD: do I want it fast, or do I want it right? Neither one is the “wrong” answer—just depends on the situation.
So, this particular time, I wanted it quick, so I chose not to TDD. But I forgot something crucial. Something that I often bring up when evangelizing TDD, as it happens. See, the primary advantage of TDD is not that it reduces bugs, that it forces you to design your interface first, that it magically creates a test suite as you develop, or that it can short-circuit some of your more insane ideas before they cost you time. No, the primary advantage of TDD is actually much simpler than that.
It reduces fear.
To explain what I mean, allow me a brief tangent. As I mentioned last time, at $work we’re working on refactoring a 10-year-old codebase with nearly 2 million lines of code. In order to be able to do this, our first step was to convince the business that we needed to slow down development of new features in order to put time into the refactor. So we had to make a business case for this. After all, just because code is old doesn’t make it bad, right? After all, as Joel Spolsky once wrote:
The idea that new code is better than old is patently absurd. Old code has been used. It has been tested. Lots of bugs have been found, and they’ve been fixed. There’s nothing wrong with it. It doesn’t acquire bugs just by sitting around on your hard drive. Au contraire, baby! Is software supposed to be like an old Dodge Dart, that rusts just sitting in the garage? Is software like a teddy bear that’s kind of gross if it’s not made out of all new material?Like so much of what Spolsky writes, this makes a lot of sense ... and yet is totally wrong. There is a problem with older code: it’s cobbled together with duct tape and bubble gum, a creaky Frankenstein’s amalgam of “there’s no time to fix this” and “here’s a better way to do that.” It has 3 or 4 different ways to implement objects, 2 or 3 different ways to read and write to the database, dozens of codepaths that are never reached at all, and dozens more that seem like they’re never reached but really are, in certain unlikely corner cases. And the end result? Fear. How many times have you started to fix something in that 10-year-old ball of spaghetti, only to pull yourself up short and say, “whoa ... if I mess this up, that’s really going to cost us some money”? How many times have you (or the business) put the kibosh on a plan to clean up this or that code, because it’s just too risky? How many times have you been yelled at because you tried to improve something and ended up inadvertently causing an obscure bug that went undetected for months? And what did you take away from those experiences?
Better to just leave it. Better to not risk improving. If it ain’t broke, don’t fix it, and, even if it is broke, that may be less costly than breaking it even worse.
Your estimates (and your actual implementation time) starts to creep up. “Oh, that part of the code is really scary; that’ll take much longer to fix.” Or, “well, there’s no one left who understands that code any more; it’ll take extra time to study it.” And this turns out to be a vicious cycle that just feeds on itself: we don’t have time to clean things up, or write good documentation, because just implementing new things takes so much longer now.
And TDD fixes that. It takes the fear out of refactoring, because you know that all your code is going to work the same way after the refactor, because all the tests will pass. If you wrote the tests after the code, passing tests wouldn’t mean that. As I constantly tell people: if I write some code, then write a test, and then run the test, and the test passes, I don’t know that my code works. Rather, I know that either my code works, or my test is broken. Sort of a 50-50 shot at correctness. But, with TDD, I know the test works (’cause it failed when I ran it before writing the code), and I know the code works (’cause the test passed after writing the code). And I can refactor with impunity, ’cause the tests guarantee the code still works. And, if I followed my TDD fairly strictly, and never wrote any code without writing a failing test first, I know all the code still works. The fear just ... melts away.
Now, realistically, does TDD guarantee that your code doesn’t have any bugs 100% of the time? No, of course not. There can be interactions between your code and other code that nobody foresaw or thought to test. There can be accidental side-effects introduced by passing code. There can be misunderstanding of the specs, so that the tests merely prove that you implemented the wrong thing correctly. There’s still plenty of places to go wrong. But, the point is, your bug rate drops dramatically. And your confidence in your code’s correctness goes to like 90%. Of course, 90% ain’t perfect. But it’s still a massive improvement over the 0% you’ve got now.
And I forgot all this, or more likely I thought it just didn’t matter. But my little task turned into 3 little tasks, and those 3 little tasks were all interrelated, and then I realized that I could build a common substructure for all 3, and that common substructure was a little complicated (not a lot, but not simple either), and all of a sudden I was making 4 or 5 Moose classes with dependency graphs in them and MAN I wished I could refactor all that crap. But I was too scared to. It was already taking longer than I meant for it to, and I didn’t want to be the hold-up on the testing, and if I tried to refactor and broke something, how would I ever know? Hell, even adding new features was starting to terrify me, because I had no way of guaranteeing that I wasn’t breaking all the stuff that I’d tested and proclaimed done.
In the end, I finished it up, and delivered it, and so far no one’s found any problems with it. But I’m not looking forward to any of my coworkers going into the code for any reason. (None of you guys read this blog, right? Ummm ...) I mean, there are parts that fairly elegant, and I think it’s moderately well commented, but still ... there are plenty of parts where someone could go “WTF did you do this for?” and I’ll just have to shake my head helplessly. ‘Cause it was too scary to try to change it, is the real answer, although I probably won’t say that. Makes you sound kinda sissy when you put it like that.
I think, next time, I’ll consider even more carefully whether to dispense with TDD or not. ‘Cause it turns out that even those “quick” projects can benefit from a lack of fear.