The Mind-killer
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 should 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.
I am sorry to say this but it is really very difficult to follow a piece of writing when someone starts talking about something they're working on and then they don't give any idea what it is that they are working on, but start talking about how they have solved a problem or had a problem in the mysterious system with some tangential thing. One thing you'll notice about Spolsky's blog is that he always gives an example of what he means.
Can I refer you to this comment:
https://blogs.perl.org/users/al_newkirk/2012/06/better-faster-funner---part-1.html#comment-173315
Buddy, this is a very good way to explain the value in testing in general and TDD in particular. Thank you!
I think the comments stand up fine without knowing anything about the app.
Joel Spolsky's article was about the folks who want to throw away what they have and start over from scratch, and I completely agree with him. Refactoring a system a bit at a time is a much safer and saner alternative.
In this particular case, the details of the project were irrelevant to the point. I chose not to describe it for two reasons: a) it would have added quite a few words to what was already a long post, and b) it would have distracted from the main point by getting everyone thinking about how they could have solved the problem differently--and probably better than I did, but, again, that wasn't the point.
Sounds like Spolsky is more your speed in bloggers, and that's okay. I'm certainly not out to be all things to all people.
Also, sorry about your aunt. :-)
@Gabor: Thanks for the kind words.
@bill.ruppert: Yes, and I agree with Spolsky on that point as well, and that's exactly the way we're approaching our own refactoring: a bit a time. That doesn't change the fact that what said about old code was 100% factual and yet twisted the truth of the matter to suit his needs at the time.
I have a bit of a love-hate relationship with Spolsky's writing, as I talked about once on my other blog. About half of what he says is utter brilliance, and the other half makes me want to strangle him. I'm sure he could care less though. :-)
I didn’t feel any need for details about what the code does – in fact, had they been included, I would have found that weird and unnecessary.
Thanks for this article. I'm also quite convinced that TDD helps and I could mention lots of examples.
However, I'd like to clarify a bit further your reasoning.
Whether the test has been written before or after the code doesn't make much difference, at least with regards to guaranteeing code robustness (writing a test case before the code actually makes the code better, that's for sure!). In fact, every test will fail initially no matter what, because you have no code to run against it, by definition.
What makes a difference IMO is that you have to see the test actually fail before fixing any bug in your code (so the code is assumed to be existing, at least :)
So, the rule of thumb is: when you have a bug, first watch the test case fail (or write the test if it doesn't exist yet). Then, and only then, fix your code and watch the test pass.
This will increase the probability :-) that the specific test case covers that specific bug.
Wow, excellent post, Buddy! Your main point, elimination of fear, is something that I wholeheartedly agree with. As I stated in a slide from my TDD presentation, the test suite serves as a "safety harness" for developers, allowing them to push forward with confidence.
Programming can be so overwhelmingly complicated and stressful: why not let the code worry about the code a bit? It gives my brain a much-needed rest. Tests help me sleep at night. ( ^_^ )
@cosimo:
Hmmm ... I'm not sure if you're agreeing with me or disagreeing with me. :-) The purpose of writing the test before the code is not so much to guarantee code robustness as to improve (I'd say "guarantee" is too strong a word) test robustness. Which indirectly helps with code robustness (it "actually makes the code better," as you say).
And of course when you say "every test will fail initially no matter what, because you have no code to run against it," that's only true if you don't write the code first, which, unfortunately, a lot of people do. So that's why I urge people not to do that. ;->
@tstanton: Thanx for the kind words Tommy! Regarding your slide, the thing I always tell people when they complain that testing is time-consuming is that writing the tests first ends up taking less time. At least it has for me. I think it's because it's much easier to figure out what test you need to write next when you don't allow yourself to write the code before the test, and you know what code you want to write next. I think when you write the code first, you have so many possibilities for testing that it's difficult to know where to start. At least that's been my experience.
(Sorry for commenting a 4 months old post...)
I fully agree on the "removing fear" aspect, since this was exactly one of the Aha! moments from my first serious experiment with TDD. Some months ago I wrote an "automator" for one of the more painful, manual processes I have to perform from time to time at work (build & package software releases for embedded targets).
I knew the Perl code would grow to several hundreds of lines, and the finished, automated process simeply HAD to work EVERY time, so I figured this would be a good time to get down and dirty with TDD. Yes, it took longer, but time and time again I made small improvements to my system while developing, and my tests proved me wrong or right.
But the best part was running the thing live for the first time: I just *knew* it would work! I had tested the darn thing hundreds of times already! It was almost an anti-climax....
No, I haven't discoverd any bugs yet, after 6 months of use. This is second benefit in my mind, the first one is the knowledge that I can (still) change things without fearing side-effects.
Thanks for a nice post, straight to the point :-)