A Dozen Reasons Why Test-First Is Better Than Test-Later (Pt. 1)
An editor of Dr. Dobbs magazine once wrote to me—replying to my response to an article—“All the benefits [of Test-Driven Development] could be attained equally by writing tests after the code, rather than before.”[1]
Tests exercise software to be sure it’s doing what was intended. So, whether you use Test-Driven Development (TDD) or write unit-tests after coding, you’re presumably getting the same benefit. The safety-net gets built, either way. Right?[2]
So, all else being equal (resulting quality, maintainability, lines of test code, lines of implementation code, time spent writing code), what advantage is there to writing the test first?
The difference between TDD and test-after unit-testing is subtle, but important. TDD is much more than writing the unit test first.
Chew on each of these separately. They frequently overlap, and I’ve even resorted to a bit of intentional repetition. These are all just words, alas: You won’t really feel it in your bones until you do TDD for a while. Think of each of the following sections as a finger pointing at the Moon: You won’t actually see the Moon until you give up on looking at the finger.
1. You cannot write untestable code.
If you do TDD well—playing the game that you cannot add any code that hasn’t been earned through a failing test—you literally cannot write untestable code.
This may seem obvious, at first, but what may not be so obvious is how easy it is to make unit-testing a bit of code harder by not writing the test first. If writing tests after were that simple, people would do it. And, they don’t, mostly.
Are you passing a non-virtual (sealed, or final) third-party dependency? How will you test all the permutations of interactions and responses with that object?
Did you create a method without a reasonable return value? (Note: Not all methods need a return value. Sometimes the most reasonable return value is none at all, aka void.)
Or (worse!) did you return an error code that the caller has to look up, or some object that the calling code now has to test against a null pointer?
Did you have a path that will throw an exception that the calling code has to deal with? Or (worse!) a Java “checked” exception that the client either has to wrap, declare as thrown, re-throw, or ignore?
Did you create a “helper” or “utility” method and, since it has no state, did you make it static? Yes, these are very easy to test. But their callers (typically someone on your team will be writing the calling code) are no longer easy to test.
Most (not all) developers tend to write perfectly reasonable procedural code when they don’t write the test first. The problem with perfectly reasonable procedural code is that it tends to be “scripty,” walking through a scenario and using branching to differentiate scenarios. Good procedural code makes for smelly object code, and bloated functional code.
TDD actually encourages us to write script code, but in the tests themselves. Each test is a representation of a possible calling sequence for client code. And it frees us up to design the implementation however we want, using the features of our programming language to its full potential: Objects, stateless functions, lambdas, abstractions, mix-ins, contracts, you-name-it.
2. You are recording your thoughts.
Objects and classes never live independent of each other. Instead, they combine and interact to create all the required behaviors. A “unit test” is a test of one tiny “unit” of that larger “business model” behavior.
The thought-process for TDD is not “Oh, let me write a test for my code…well how can I do that if I haven’t written the code?” Here’s a more common internal dialog: “I need this particular object to provide this next new bit of behavior. When I give the object this particular state, then ask it to act on that state, here are results I expect.”
Note that this internal dialog naturally occurs from a viewpoint external to the object. You’re recording a mini-specification for that new behavior in a simple, developer-readable, re-runnable automated test.
3. We design the interface from the viewpoint of calling code.
With TDD, often the required object class, or its methods, don’t even exist yet; so we’re effectively designing the naming and the interface (public method signatures) to that object through the tests. The unit tests are the first “clients” of that behavior. In this way, our interfaces (i.e., what objects are designed to do for other objects) are designed from the correct perspective: Each from the caller’s perspective.
4. You have to know the answer.
Perfectly rational, professional developers, when faced with making a decision with insufficient information, will often choose something that “feels right,” and plan to ask for clarification later—presumably during their copious free time, at a very relaxed meeting full of open dialog, during some testing and “hardening” phase that never seems to happen.
Computer programming does not do well with vagaries. Computers will do what you tell ‘em to do; nothing more, nothing less, and nothing different.
So if you don’t know for which day to journal a transaction that happens exactly at midnight, you’d better go find out! Ask the product advocate, business analyst, tester, or your pair-programming partner. If no one seems to know, then we need to ask an actual customer.
So we can’t write the test unless we know the answer we’d expect. You have to get out the calculator, slide-rule, or Google; or preferably the product advocate has told you what to expect (preferably through a Cucumber scenario).
What do we tend to do if we simply write out the whole 10240-bit quantum-encryption algorithm first, them write a unit test for it? We may assume the answer (which would look like random static anyway) our own code gives us is correct! And that, folks, is a huge disaster waiting to happen.
We’re more likely to look for the right answer before ever writing the solution to that request, than we are to ask questions after we’ve already written the code.
While we wait…
That’s the first 4 of 12. The next batch will be out in a month. Comments so far? Thanks for reading! “See” you next month!
Footnotes
[1] http://www.drdobbs.com/architecture-and-design/addressing-the-corruption-of-agile/240166890
[2] Lauri Williams did a study comparing test-first with test-after. The test-after team was finished before the test-first team. Alas, their code had more defects, because they didn’t sufficiently unit-test the code. In other words, they cheated. Not because they were cheaters, but because unit-testing code after the fact is harder…and boring. And you’re far more likely to miss an important case. https://collaboration.csc.ncsu.edu/laurie/Papers/TDDpaperv8.pdf
[…] The full article is available here. […]
Very timely! I’ve been looking for some selling points on TDD. Thanks, Rob.