Sunday, October 11, 2015

TDD and Getting Lost in the Trees (Part 1)

"Experience enables you to recognize a mistake when you make it again." – Franklin P. Jones

This is the fifth of a series of posts about Test-Driven Development (TDD).  Here's a recap of what I've posted so far:


As promised in my last post, I'm going to show some code examples this time. But before we get to that, bear with me as I wax philosophical just a little bit more.

How do we get ourselves in trouble? Let me count the ways…


I've seen many ways that programmers, including myself, can paint themselves into a corner. You have to admit, we can be pretty good at getting ourselves wedged between a rock and a hard place and I wouldn't be surprised if you found a mess of other ways besides the ones that I'll put under a microscope here. (Quick, how many idiomatic expressions did I manage to squeeze into this paragraph?)

I started out writing this post thinking that I could cover several scenarios but it quickly became clear that this first one that I'm going to bring up has so many facets to it that it deserves to be picked apart in isolation. Buckle up because this is going to be a doozy!

Recognizing that we don't know what we don't know


If you're one of those who I predicted would struggle with TDD and the simple calculator problem, then you probably didn't do so well on your first few attempts, even if you think you did.

You might think it a bit presumptuous of me to say that or maybe even arrogant but if you think about it for a minute, it's really a natural consequence of just starting out with TDD. Without a frame of reference, how can you know if what you just did was really what TDD is supposed to be like? If you're relatively new to TDD, then you won't really know that you've done it poorly until you do it better. As Donald Rumsfeld famously put it, "There are things we don't know we don't know."

This is kind of what the phrase "Aikido works, your Aikido doesn't" is getting at. It's what we sometimes tell people who think Aikido is ineffective. Similarly, people who say the same about Agile are sometimes told that "Agile works, your Agile doesn't." Or with TDD, "TDD works, your TDD doesn't." I honestly don't say this often because it tends to rub people the wrong way and often the people you want to say it to the most aren't really worth the aggravation. Nevertheless, there is a ring of truth to it that can't be denied and maybe that's why they get mad, because now they know it's them.

When I was just starting to learn Aikido, I got better by pairing up with black belts because they would usually point out things that I could improve, things that I would have never recognized otherwise. Now that I'm a black belt myself, I try to pay it forward by helping new students recognize ways they can improve their practice.

Remember, practice only makes habit. Only perfect practice makes perfect. Unless we start recognizing aspects of our practice that are flawed or could be strengthened, we're not going to be able to make it better, much less perfect. So, here are some things that I first focused on recognizing and making better in my TDD practice. Hopefully, these will help you see how you can get better, too.

Getting Lost in the Trees


You've probably heard the expression "seeing the forest for the trees". This first scenario that we will examine is about losing sight of the forest and getting lost in the trees. There are many dimensions to this but it happens quite often and it mainly comes from not understanding the principles that Michael Feathers and Steve Freeman alluded to in their presentation "Test-Driven Development: Ten Years Later." It is rooted in at least two thought processes that I think most developers find difficult to avoid. These are boxes that most of us find hard to think out of.

(Grammar police wannabes be like, "Really, ending a sentence with a preposition?" and I'd be like, "Chill out, dude, don't get all Carol with me. It's fine." Carol is hilarious though. If you're not quite following me, I'm talking about the TV comedy show, "The Last Man on Earth")

First, let's take a look at some test code that's typical of when developers are getting themselves "lost in the trees."

// CalculatorTest.java

public class CalculatorTest {

   private static final double TOLERANCE = 0.0001;
 
   private Calculator calc;
 
   @Before
   public void setUp() {
      calc = new Calculator();
   }

   @Test
   public void testAddition() {
      assertEquals(3.0, calc.evaluate("1 + 2"), TOLERANCE);
   }

   @Ignore
   @Test
   public void testMultiplication() {
      assertEquals(6.0, calc.evaluate("2 * 3"), TOLERANCE);
   }

   @Ignore
   @Test
   public void testAdditionAndMultiplication() {
      assertEquals(7.0, calc.evaluate("1 + 2 * 3"), TOLERANCE);
   }
 
   @Ignore
   @Test
   public void testWithParentheses() {
      assertEquals(9.0, calc.evaluate("(1 + 2) * 3"), TOLERANCE);
   }
}

// Calculator.java

public class Calculator {
   
   public double evaluate(String expression) {
       return 0.0;
   }
}

If you're thinking that this code doesn't look all that bad, you should probably pay close attention to the rest of this post. You might learn a few things you don't know you don't know. And no, that's not a typo; see the Donald Rumsfeld quote above.

If you're thinking that this is way too much code to start with when you're doing TDD, it's great that you recognize that. You're right, if this is the code that you have after just one or two TDD cycles, then you're probably writing too much up front. But just humor me for a bit. Let's just say for now that this is code you might have after a few rounds of following the Red-Green-Refactor flow. Then we'll step through how we might get here.

They say that imitation is the greatest form of flattery so I'm going to fashion this after Uncle Bob's archetypical XP Episode. Since I already mentioned Carol, let's get Phil in on this and pretend that they are pairing up on the calculator TDD exercise. Suspend your disbelief as necessary.

(Phil, of course, is the title character in "The Last Man on Earth")

Phil: Hey, Carol, I started that calculator TDD exercise like you asked me to. Here's the code so far. I even have my first failing test. See?

Carol: Really, Phil? That looks like a lot of code. Did you really get that through proper TDD or did you just write it all at once because it's what you thought you'd need anyway?

Phil: (sheepishly) I wrote it all at once because it's what I thought I'd need anyway...

Carol: That's Ok, Phil, I understand. You just wanted to save some time and cut to the chase. But it's important to understand that the way we get there is just as important as what we get in the end when we do TDD. The means is just as important as the end, Phil. Remember that.

Phil: I don't know what we're getting out of doing all those little steps though, Carol. It seems like a waste of time and it feels silly to do the wrong things when the right things are so obvious.

Carol: Oh, like how you knew that NOT leaving Todd in the desert to die so you could have more booty for yourself was obviously the right thing to do?

Phil: Aw jeez, Carol, that was a low blow, even for you. I already said I was sorry! And I went back for him, didn't I?! Am I ever going to live that mistake down? That was one small lapse of judgement in a moment of weakness!

Carol: Relax, Tandy. That's all behind us now but you should remember the lessons from the past. Doing TDD reminds us that you often have to see the wrong thing before you realize what the right thing to do is. Tell you what, why don't we write that calculator again and this time, let's go through the TDD cycle and thought process, step by step.

Phil: (grudgingly) Ok. Lucky for us, I'm using Git and I just happened to make that default JUnit test my initial commit. Let me revert to that really quick.


// CalculatorTest.java

public class CalculatorTest {

   @Test
   public void test() {
      fail("Not yet implemented");
   }

}

(PAUSE)

Let's hit the pause button for just a minute and reflect on what just transpired. You might be thinking that this conversation is a bit contrived (Well, duh. Unlike Uncle Bob and Bob Koss, Phil and Carol are not real people) but it's based on observations and questions from my own experience with doing TDD and teaching it to other developers.

Reflection #1: Phil went ahead and just wrote all the test code that he thought he was going to need. His intention was to save time. He also felt that the small intermediate steps were a waste of time and that it was silly because he was writing code that was obviously wrong.

The thing to recognize here is that writing correct code and avoiding or eliminating incorrect code is part of the complex algorithm for writing a program that you've followed for years. Your brain has most likely also been trained to be averse to rework, based on the belief that rework is wasteful and costly and that you can avoid rework by doing things right the first time.

Does that strike a chord with you? Do you not hold these things in high regard, if not dearly, as a software developer?

These ideas form part of your understanding of what it takes to write good software. It's the same kind of understanding that you have about how to ride a bicycle. It is the kind of understanding that is so entrenched in your brain that it has become almost a second nature to you. It's that rigid idea that Destin said is stuck in your head and you're finding it hard to change it, even if you want to.

Destin said that "Knowledge is not understanding." In other words, understanding runs deeper than knowledge. Much deeper. That's why it's so hard to change. Even if you have the knowledge about how to write programs through TDD, it's going to take a lot of effort to supplant your brain's current understanding of how to write programs with a new understanding that comes from all that TDD knowledge.

Does that make sense?

Reflection #2: TDD is as much about the way you get there as it is about what you get in the end.

Very skilled programmers can probably produce software with the same level of quality and testing that you could get by doing TDD properly. The problem is, I don't think I've ever met one these mythical super rock stars of programming in real life, even after decades in this profession. I've worked with many companies and met many developers and from what I can tell, the average developer just doesn't write very good programs and they don't write very good tests, sometimes even when they claim to be doing TDD. Color me cynical but I'm just stating my opinion based on experience.

If you recall what Michael Feathers and Steve Freeman said in their presentation, TDD is much more than just testing. Each of the steps you take in the TDD cycle has a very distinct purpose and focus. And that's why the path to get to the end product is very important. More about this later.

Reflection #3: TDD reminds us that we often need to see what's wrong before we can recognize what's right. You've probably heard or said this before: "I'll know it when I see it." We say this because it's probably the case that

  1. You've never really seen "It" before
  2. "It" is just a vague notion or an immature or incomplete idea
  3. You can't completely describe or wrap your head around what "It" is
  4. There are a few things "It" could be; there are multiple possibilities for "It"
  5. You have to actually try some of the possibilities for "It" first before you can say what "It" is

Take a minute to reflect on the above again before we hit the play button to see what happens next with Phil and Carol.

(PLAY)

Phil: Ok, first step is to write some test code and see it fail. I'm going to need a Calculator class. Agreed?

Carol: Sure, Phil, I agree.

Phil: (sigh) This is going to be a long day. (makes changes to the code)

// CalculatorTest.java

public class CalculatorTest {

   @Test
   public void test() {
      Calculator calc = new Calculator();
   }
}

Phil: Ok, I'm instantiating Calculator but this doesn't compile because we don't have a Calculator class yet. That still qualifies as Red because according to Uncle Bob's Three Rules of TDD, code that doesn't compile is still a failure.

Carol: Yup, you got it. Go ahead and fix it then.

Phil: (uses keyboard shortcuts) There. Now the test compiles.

Carol: Oooh, now you're just showing off, Phil. But that's cool. There's nothing more annoying than mousing around for every little thing you need to do. The keyboard shortcuts are much faster. Ok, run the test then.

Phil: What do you mean? There's nothing in the test to run.

Carol: Oh, Ok. Well, I suppose we should write another line of test code then.

Phil: See, this is getting pretty silly now. Why do we have to do this when we can easily just write everything out and then fix the problems all at once? This is a really tedious way to write a program, isn't it?

Carol: Believe me, Phil, I've been there. It really does feel tedious and downright stupid at first but once we get a hang of the flow and a better idea of where we're going, we can start trying to write bigger chunks of code. Right now, we're writing tiny chunks of code to force ourselves to take things slow. Really slow.

Phil: Why do we need to go so slow?

Carol: Because we want to fail fast and fail small.

Phil: Wait, what? I thought we were going slow. Now we want to go fast? WTH, Carol! Make up your mind! Are we going slow or fast?

Carol: Silly Care Bear. I know that sounds contradictory but by "going slow," I mean that we write smaller increments of new code. Smaller increments means that it'll take us longer to get all the code that we eventually want to get. That's because we're failing fast and failing small, which means that we run our tests every time we add a small increment of code. This way, we'll know right away if we messed anything up. And if we do mess anything up, we know it's probably the last small increment of code that messed it up. Bottom line is that we go slow by adding only small bits of code, fail fast by always running the tests and getting feedback right away, and fail small because we can only mess up a little bit with each increment of code.

Carol: It's kind of like driving in the dark, right? If you go fifty miles before checking if you're still on the right road, then you can drive faster but there's a bigger chance that you can go miles out of your way and get really lost. But if you stop every five miles to check with someone at a store or gas station if you're still on track, then it might take you longer to get wherever you're going, but at least you won't get too far off track at any point. Does that make sense?

Phil: That driving thing makes sense because that's kind of what I did when I took scenic routes from Billings, Montana back home to Tucson. I think I follow your twisted logic though, Carol. You still might have to explain it to me again a few times. I'm not sure I really understanding it all.

Carol: That's Ok, Phil. At least now you know. Understanding will come with practice. You just gotta keep doing it for a while for it to really sink in and make sense. And when it does, it does. It's kind of like those 3D pictures that you have to stare at cross-eyed, you know? It's hard to see the 3D at first but once you get it, you get it and it's a lot easier to see the 3D in other pictures afterwards.

Phil: Huh, I guess I never really looked at it like that before. Yeah, it took me a while to get those 3D pictures. I always saw them as just random patterns until one day, I just suddenly saw the 3D in one of them. Ok, I'll try to see this through some more. Now I'm curious to see if this picture of TDD that you have is as great as you say it is.

(PAUSE)

I'm going to hit the pause button again here for some more reflection.

Reflection #4: Adding small increments of code may seem silly and tedious but it allows you to go slow and keep checking your progress more often. I think Carol explained it quite well.

Reflection #5: Fail Fast and Fail Small. Run your tests every time you add an increment of code so you get feedback quickly and often. The smaller your code increments, the smaller the problems are that they might introduce. The smaller code also makes it easier to revert to a previous state where all the tests were passing.

Reflection #6: Knowledge is NOT Understanding. Understanding will come later, when the knowledge you have is exercised in a context. The more contexts there are in which you apply that knowledge, the more it gets ingrained in your brain as Understanding.

Reflection #7: The tests provide different contexts in which you can exercise your knowledge of what your program is doing. The more you test your program, the more you understand what it's doing. See Reflection #6.

Reflection #8: The algorithm for writing a program with TDD is very complex and complicated and belies the simplicity of the Red-Green-Refactor mantra. As you can see from the number of things we can reflect upon from just this short "conversation," there are many things to think about when you're doing TDD.

You may be asking where those "trees" are that we supposedly can get lost in. If you don't see them, then remember what I said before about not having enough experience to recognize what you don't know? The trees are there but maybe you just didn't recognize them for what they are.

I know you're probably disappointed to not see more code than what I've shown but if you have a little bit more patience, it will be rewarded. (Remember what I said in the first post about needing some virtues?)

We'll continue our eavesdropping on Phil and Carol's TDD session next time. They are going to be writing a lot more now that they have a bit of a shared understanding of TDD.

In the meantime, I encourage you to reflect on all this some more. As you do that TDD exercise again, keep in mind whatever new knowledge you've managed to pick up here. This is how you develop recognition, which in turn opens up possibilities for improvement and getting closer to perfection in your practice of TDD.

Next: TDD and Getting Lost in the Trees (Part 2)

Post a Comment