A Dozen Reasons Why Test-First is Better than Test-Later, Pt. 3

The third and final part in the Developer Essentials mini-series of posts about test driven development (TDD). Click here, if you missed Part 1 or Part 2.

9. You cannot mistakenly leave behind untested/undocumented code.

TDD doesn’t just change the order of tasks: it makes writing the unit test an integral part of writing the code.

The behaviors of your system are recorded in the tests. The way the system accomplishes those behaviors is in the implementation. Those are closely intertwined, of course, but they are different perspectives on the same behaviors. Tests tend to be easier for us mere mortals to follow, because they are simple step-by-step scripts, whereas any bit of implementation may have numerous paths, assumptions, responsibilities, and—perhaps most importantly—things that it’s not supposed to be doing.

Very early on in my career (circa 1990), I was tasked with looking for inefficiencies in our implementation of a pre-TCP/IP high-speed networking protocol. I found a rather complex-looking variable-length message buffer-padding algorithm… [waves hands wildly…it was, after all, 30 years ago].

It wasn’t slow, but it seemed a bit convoluted, and certainly difficult to decipher. I recall determining that I could reduce the if-then-else menagerie into one simple equation. “I’m so clever!”

But it turns out (because…no unit tests to serve as engineering spec, or safety-net!) that my calculation failed to consider that a “zero-byte” message still needed at least one empty message-body “block” sent. Now that may have been wasteful from a data transfer perspective, but one has to consider that (a) “empty” messages (mostly control messages) were rare compared to the massive amounts of data that were being transferred, and (b) it was actually part of the protocol. My calculation would have sent over no blocks, and the other end of the protocol would have—very rapidly, on a liquid-cooled Cray supercomputer—lost it’s mind.[8]

The original code could have had a comment that said, “This code cannot be reduced further because of the zero-length-message case.” But it didn’t.

Or there could have been a single very simple unit test for the zero case, which would have failed when I made my “amazingly insightful” change. But there wasn’t.

Not to leave you hanging with deep concern over the fate of 1990s-era national security: I was so amazed that my colleagues had missed this elegant solution that I ran into the boss’s office and told him about it. He turned to me and (very calmly) said, “What about the zero-length message case…?”[9]

10. TDD is more rewarding.

It’s the game-ification of what would otherwise be an unpleasant chore. Let’s face it, writing unit tests (the chore) after the code (the fun part) is akin to eating cold vegetables after your dessert. With TDD, unit-testing is actually part of the fun of writing code.

With each failing test-scenario written, we “earn” the right to add a little bit more behavior to your system. We then take a swing at writing the implementation that will pass the test, and we get instant rewards in each passing unit-test.

Don’t ever forget: Humans write software. You are human. We like games. We respond well to fast, pleasant feedback. TDD is closer to a game than test-after. Test-after is a chore. Sure, professional developers need to be unit-testing their code, so they shouldn’t shirk doing their chores. But we’re still human. If we can make the chore a fun game, where’s the harm?

11. It’s very difficult to write smelly code.

Beck’s Four Rules of Simple Design may read like rules, but they’re more like obvious natural outcomes if you’re doing TDD. Believe me, I’ve tried to write stinky code test-driven, in order to create a sufficiently challenging refactoring lab. In the TestedTrek lab, the tests were written afterwards. Why is this? Small, discrete, independent tests tend to lead you naturally to write code that separates concerns without duplication, using the features of your programming language. I tried to write TestedTrek test-first, but in order to stink up the code I’d have to go back and alter the degree of isolation in the tests. I gave up and instead created MessTrek (an untested ball of mud) first, then added legacy characterization tests onto MessTrek to create TestedTrek.

12. TDD is not a testing technique.

Test-first turns unit-testing into a thinking tool, and a development process; not just a testing technique. Each test isn’t really a test until it passes for the first time. Before that, it’s an individual engineering specification: a unique little scenario describing a behavior that you want from your code. Software developers naturally “think in tests” or perhaps more accurately we think in “discrete scenarios.” Now we’re encouraged to actually write out each of those scenarios, justifying every addition to the code. Once you’re used to it, you won’t want to write code without a failing test scenario.

Conclusion

So while it’s true that—if your code is well-designed, and that’s a big “if”!—you can write the tests afterwards and get the same benefits from having the unit tests. But you’ll be missing the benefits of that early ease, safety, cleanliness, confidence, and fun.


I hope you’ve enjoyed this mini-series of newsletters. Let me know by connecting with me at one of the following cyber-worlds:

Thanks for reading, and “see” you next month!


[8] This was the 1990s, when Crays were the fastest system around, and HYPERchannel was the only way you could get a Cray to talk to your mainframe without falling asleep from sheer boredom. 50 Megabits/second!

[9] If only we had been pair-programming! Oh, the time we would have saved that day.

Related Articles

Responses