TDD: Simplest is not Stupidest
Where the Message Gets Lost In Teaching TDD
I recently answered a programmers’ stackexchange post about test-driven development. (As an aside, it will be cool when me linking to a SE question drives more traffic their way than them linking to me drives my way 🙂 ). As I’m wont to do, I said a lot in the answer there, but I’d like to expand a facet of my answer into a blog post that hopefully clarifies an aspect of Test-Driven Development (TDD) for people–at least, for people who see the practice the way that I do.
One of the mainstays of showcasing test-driven development is to show some extremely bone-headed ways to get tests to pass. I do this myself when I’m showing people how to follow TDD, and the reason is to drive home the point “do the simplest thing.” For instance, I was recently putting together such a demo and started out with the following code:
[TestMethod, Owner("ebd"), TestCategory("Proven"), TestCategory("Unit")]
public void IsEven_Returns_False_For_1()
{
var inspector = new NumberInspector();
Assert.IsFalse(inspector.IsEven(1));
}
public class NumberInspector
{
public bool IsEven(int target)
{
return false;
}
}
This is how the code looked after going from the “red” to the “green” portion of the cycle. When I used CodeRush to define the IsEven method, it defaulted to throwing NotImplementedException, which constituted a failure. To make it pass, I just changed that to “return false.”
The reason that this is such a common way to explain TDD is that the practice is generally being introduced to people who are used to approaching problems monolithically, as described in this post I wrote a while back. For people used to solving problems this way, the question isn’t, “how do I get the right value for one,” but rather, “how do I solve it for all integers and how do I ensure that it runs in constant time and is the modulo operator as efficient as bit shifting and what do I do if the user wants to do it for decimal types should I truncate or round or throw an exception and whoah, I’m freaking out man!” There’s a tendency, often fired in the forge of undergrad CS programs, to believe that the entire algorithm has to be conceived, envisioned, and drawn up in its entirety before the first character of code is written.
So TDD is taught the way it is to provide contrast. I show people an example like this to say, “forget all that other stuff–all you need to do is get this one test passing for this one input and just assume that this will be the only input ever, go, now!” TDD is supposed to be fast, and it’s supposed to help you solve just one problem at a time. The fact that returning false won’t work for two isn’t your problem–it’s the problem of you forty-five seconds from now, so there’s no reason for you to bother with it. Live a little–procrastinate!
You refine your algorithm only as the inputs mandate it, and you pick your inputs so as to get the code doing what you want. For instance, after putting in the “return false” and getting the first test passing, it’s pretty apparent that this won’t work for the input “2”. So now you’ve got your next problem–you write the test for 2 and then you set about getting it to pass, say with “return target == 2”. That’s still not great. But it’s better, it was fast, and now your code solves two cases instead of just the one.
Running off the Rails
But there is a tendency I think, as demonstrated by Kristof’s question, for TDD teachers to give the wrong impression. If you were to write a test for 3, “return target == 2” would pass and you might move on to 4. What do you do at 4? How about “return target == 2 || target == 4;”
So far we’ve been moving in a good direction, but if you take off your “simplest thing” hat for a moment and think about basic programming and math, you can see that we’re starting down a pretty ominous path. After throwing in a 6 and an 8 to the or clause, you might simply decide to use a loop to iterate through all even numbers up to int.MaxValue, or-ing a return value with itself to see if target is any of them.
public bool IsEven(int target)
{
bool isEven = false;
for (int index = 0; index < int.MaxValue - 1; index += 2)
isEven |= target == index;
return isEven;
}
Yikes! What went wrong? How did we wind up doing something so obtuse following the red-green-refactor principles of TDD? Two considerations, one reason: "simplest" isn't "stupidest."
Simplicity Reconsidered
The first consideration is that simple-complex is not measured on the same scale as stupid-clever. The two have a case-by-case, often interconnected relationship, but simple and stupid aren't the same just as complex and clever aren't the same. So the fact that something is the first thing you think of or the most brainless thing that you think of doesn't mean that it's the simplest thing you could think of. What's the simplest way to get an empty boolean method to return false? "return false;" has no branches and one hardcoded piece of logic. What's the simplest way that you could get a boolean method to return false for 1 and true for 2? "return target == 2" accomplishes the task with a single conditional of incredibly simple math. How about false for 1 and true for 2 and 4? "return target % 2 == 0" accomplishes the task with a single conditional of slightly more involved math. "return target == 2 || target == 4" accomplishes the task with a single conditional containing two clauses (could also be two conditionals). Modulo arithmetic is more elegant/sophisticated, but it is also simpler.
Now, I fully understand the importance in TDD of proceeding methodically and solving problems in cadence. If you can't think of the modulo solution, it's perfectly valid to use the or condition and put in another data point such as testing for IsEven(6). Or perhaps you get all tests passing with the more obtuse solution and then spend the refactor phase refining the algorithm. Certainly nothing wrong with either approach, but at some point you have to make the jump from obtuse to simple, and the real "aha!" moment with TDD comes when you start to recognize the fundamental difference between the two, which is what I'll call the second consideration.
The second consideration is that "simplest" advances an algorithm where "stupidest" does not. To understand what I mean, consider this table:
In every case that you add a test, you're adding complexity to the method. This is ultimately not sustainable. You'll never wind up sticking code in production if you need to modify the algorithm every time a new input is sent your way. Well, I shouldn't say never--the Brute Forces are busily cranking these out for you to enjoy on the Daily WTF. But you aren't Brute Force--TDD isn't his style. And because you're not, you need to use either the green or refactor phase to do the simplest possible thing to advance your algorithm.
A great way to do this is to take stock after each cycle, before you write your next failing test and clarify to yourself how you've gotten closer to being done. After the green-refactor, you should be able to note a game-changing refinement. For instance:
Notice the difference here. In the first two entries, we make real progress. We go from no method to a method and then from a method with one hard-coded value to one that can make the distinction we want for a limited set of values. On the next line, our gains are purely superficial--we grow our limited set from distinguishing between 2 values to 3. That's not good enough, so we can use the refactor cycle to go from our limited set to the complete set.
It might not always be possible to go from limited to complete like that, but you should get somewhere. Maybe you somehow handle all values under 100 or all positive or all negative values. Whatever the case may be, it should cover more ground and be more general. Because really, TDD at its core is a mechanism to help you start with concrete test cases and tease out an algorithm that becomes increasingly generalized.
So please remember that the discipline isn't to do the stupidest or most obtuse thing that works. The discipline is to break a large problem into a series of comparably simple problems that you can solve in sequence without getting ahead of yourself. And this is achieved by simplicity and generalizing--not by brute force.






Jan
25
By Erik Dietrich
Comments: Here’s my Code and I’m Sorry
Category: Language Agnostic Tags: Comments, Metaphors for Software, Software Engineering | 6 Comments
Heavily Commented Code: The Awful Empathy
Imagine a feeling–that empathy you get when you’re watching someone fail in front of an audience. Perhaps its a comedian relying on awful puns and props or a person in a public speaking course that just freezes after forgetting some lines. Ugh. It’s so awkward that it hurts. It’s almost like you’re up there with them. You start mumbling to yourself, “please, just be funny,” or, “please, just remember your lines.”
Think of how you feel when someone gets up to talk at a meeting or give a presentation, and they start off with something like, “okay, this isn’t really finished, and Jones was supposed to help, but he got assigned to the Smith contract, and I did things in here that I’m not proud of, and…” With every clause that the speaker tacks onto the mounting lists of reasons that you’re going to hate it, you feel your discomfort mounting–so much so that you may even get preemptively angry or impatient because you know he’s going to bomb and you’re about to feel that tooth-grinding failure-empathy.
Okay. Now that the stage is set and we’re imagining the same feeling, know that this is how I feel when I open code file and see the green of comments (this is the color of comments in all my IDEs) prominently in a file. It’s as though the author of the code is on a stage in front of me, and he’s saying, “okay, so this is probably not very clear, and some of this is actually completely wrong, and changing this would be a nightmare, and you might want a beer or two, heh heh, but really, this will make more sense if you’re drunk, and, you know what, I’m sorry, really, really sorry because this is just, well, it’s just… it’s complete garbage. Sorry.”
That might seem a bit harsh, but think of what you’re looking at when you see code with a comment to actual code ratio approaching 1:1. You’re looking at a situation where someone needed to spend as much space and probably as much time trying to tell you what the code says as writing the code. Why would someone do that? Why would someone write a bunch of code and then write a bunch of English explaining to someone fluent in code what the code does? This is like me sending you an email in Spanish and putting the English equivalent after every sentence. I would do it if one of the two of us didn’t speak Spanish well or at all. And that’s how I feel when I see all those comments–either you don’t speak code very well or you think that I don’t speak code very well. The former occurs a lot with people who program haphazardly by coincidence. (“I better write this down in a comment because I had no idea that’s what an array was for. Who knew?”) The latter generates mind-numbing comments that rot. (“Declares an int called x and initializes it to 6.”) If you aren’t being forced to write comments by some kind of style policy and you’re not Zorro, you’re writing things in English because you’re not bothering to write and illuminate them in Code (I’m using the uppercase to distinguish simply writing some kind of compiling code from writing Code that communicates).
Self-Fulfilling Prophecy
There’s a substantial cross section of the developer world that thinks diligently commenting code is not only a best practice, but also table stakes for basic caring and being good at your craft. As I’ve previously explained, I used to be one of those people. I viewed writing comments in the same way that I view shutting drawers when I’m done using them or making my bed–just grunt work that’s unavoidable in life if you want to be a person that’s organized. Interestingly, I never really viewed them as particularly communicative, and, since adopting TDD and writing tiny methods, I viewed them largely as superfluous except for occasionally explaining some kind of political decision that transcended code (documenting APIs that you’ll release for public consumption are also an exception as this becomes part of your deliverable product). But I started to get increasingly disillusioned with the way comments would look in group code.
I would return to a method that I’d written six months earlier and for which I’d put a very clear doc comment, only to see something like this:
Holy crap! We're clearly playing Russian Roulette here. Did the requirements change and we're no longer endangering puppies? Is this code causing terrible things to happen? Who wrote that comment about 15? Hopefully not the same person that wrote the next line! And what should this code do--what the various comments say or what it actually does? Do the people responsible even work here anymore?
I'm pretty sure that anyone reading is chuckling and nodding sympathetically right now. You can only return to a method that you've written and see this sort of thing so many times before you come to a few conclusions:
Surely this isn't really news to anyone, and it's probably answered with admonishments and mental notes to be more diligent about updating comments that everyone knows won't actually happen. So why is it then considered good form and often mandated to put lies into source control for the later 'benefit' of others? Why do we do this, discounting the bed-making/diligence/good-citizen motivation?
To answer that question, think of the kind of code where you see comments and the kind of code where you don't. If you see a four-line functional method with a single nested loop and no local variables, do you generally see comments in there? Probably not. How about a fifty line method with so many nested control structures that you need some kind of productivity add-in to know if you're scoped inside that else you saw earlier or another if, or maybe a while loop? Bingo--that's where comments go to hang out. Giant methods. Classes with lots of responsibilities and confusing internal state. Cryptically-named local variables. These things are all like cool, dank yards after a storm, sprouting explanatory comments like so many mushrooms. They sit there in mute testimony to the mildew-ridden, fungus-friendly conditions around them.
To put it another way, comments become necessary because the author isn't speaking Code well and punts, using English instead of fixing the code to be clear and expressive. Thus the comments are compensation for a lack of clarity. But they're more than that. They're an implied apology for the code as well. They're an apology for writing code and not Code. They're an apology for the fact that writing code and not Code results in the project being a legacy project before it's even done being written. They're an implied apology for big, lumbering classes, winding methods, confusing state, and other obfuscations of intent. But most of all, they're the preemptive, awkward-empathy-inducing, "hang onto your hat because what I'm doing here is actually nuts" pre-apologies/excuses to anyone with the misfortune of reading the code.
So please, I beg you--next time you find yourself thinking, "dude, nobody's ever going figure this wackiness out unless I spend a few sentences explaining myself," don't bother with the explanation. Bother instead to correct the "nobody's ever going to figure this out" part. Good Code speaks for itself so that you can focus on more important things.