Tuesday, March 9, 2010

TDD Tidbits: Red, Green, REFACTOR!!!

So I was a TDD noob at one point. I had to start somewhere so I picked up some recommended reading, Kent Beck's Test Driven Design: By Example. I'll be honest; I didn't like it. I actually returned the book. Was I stupid? No! It just didn't seem to add up.

To Mr. Beck's benefit, I wasn't rejecting his book but rather the notion that I should put myself on a leash when I code and do "stupid" stuff like return static data to satisfy a test. I was feeling the discomfort a lot of developers feel when they first dive into this stuff. It's a whole different way of writing code. I would never fault anyone for having reactions like my own.

The idea of returning junk data to satisfy a test was probably the hardest thing for me to overcome. I spent many frustrating sessions trying to figure out when to stop returning dummy data or why I was even doing it. It was frustrating to know how to code things but having to hold myself back and continue with the TDD process. It won't let you wander off and start implementing all sorts of code.

Why the hell would I return dummy values in my production code? Why would I fake out my code at all? I could write code all day that would return dummy data. Hey look, my code returns zero! On the next unit test I can have it just return one and fake that out as well! What the hell is this proving? I'm not writing any meaningful code! I KNOW what the answer is. Why do I have to go about it this way? What a waste of time! *head explodes*

To speak to my frustrations I didn't fully understand the process and I didn't (immediately) see the benefits. I was missing the important step of refactoring my code after the test passes. Sure, you may write a test that returns a static value of zero. Then you write another test that expects the code to behave a new way and return another value. At this point, feel free to refactor your guts out. You don't need permission to write code. Don't let the dogma of "only write code when you have a failing test" fool you. It comes in two stages. You write code to get the test to pass and then you write code again to refactor! Your test passes but that doesn't mean all hands have to come off the keyboard!

For example, here's a bucket. You put apples in the bucket and then you want to know how many apples you have. The first test and implentaton may look like this.


public class BucketFixture
{
[Test]
public void Should_have_five_apples_in_bucket()
{
Bucket bucket = new Bucket();
bucket.AddApples(5);

bucket.TotalAppleCount.ShouldEqual(5);
}
}

public class Bucket
{
public void AddApples(int appleCount)
{
}

public int TotalAppleCount
{
get
{
return 5;
}
}
}

Then you want another test to hammer out more of the behavior of this awesome bucket.


public class BucketFixture
{
[Test]
public void Should_have_five_apples_in_bucket()
{
Bucket bucket = new Bucket();
bucket.AddApples(5);

bucket.TotalAppleCount.ShouldEqual(5);
}

[Test]
public void Should_have_no_apples_in_bucket()
{
Bucket bucket = new Bucket();

bucket.TotalAppleCount.ShouldEqual(0);
}
}

The new test breaks as expected but now you can't pass back dummy data. What code can you write to satisfy both tests?


public class Bucket
{
int _totalAppleCount;

public void AddApples(int appleCount)
{
_totalAppleCount += appleCount;
}

public int TotalAppleCount
{
get
{
return _totalAppleCount;
}
}
}

Huzzah! But I don't necessarily have to stop there. I can still refactor if my heart so desired. I don't need a failing test. What if I wanted to be super cool and use auto properties? Don't bother to write a new test for it. Just do it.


public class Bucket
{
public void AddApples(int appleCount)
{
TotalAppleCount += appleCount;
}

public int TotalAppleCount
{
get; private set;
}
}

No failing test required! But bear in mind this is refactoring; we aren't changing behavior, only structure. The code must continue to behave the same. The tests will verify that.

Another thing to point out is that if the only behavior the bucket exhibited was that it had 5 apples then the test and implementation stops immediately. This is when it's called out that you're trying to do the simplest thing to satisfy the requirements. Even when new behavior is added, you're taking the shortest route to functional without being a complete chimp about it. Add patterns where applicable and such.

Last point I'll make is that now I view my first test as probably the most important to my API. The first test is figuring out the names of my classes, interfaces, properties, methods and arguments. It's the first stab at the design of the class that you're making. This is where I'm extra sensitive to the needs of the client code (typically called dog fooding). It's a big deal to me that my code demonstrates its intent and usage without the need for lengthy comments.


So there you go kids. Take advantage of the refactoring step! Do all the stuff that you want to do when you think that the TDD process has put you on a leash.

2 comments:

OMouse said...

You actually missed a more important property of the code. You were testing individual cases, 5 and 0, when you could have been testing the set of all integers greater or equal to zero. That's the expected result that you need to extract from those individual cases. Once you know the set of all possible outputs, you can work backwards and write your function.

Nick Swarr said...

I wouldn't get too carried away with the example. I used it to demonstrate the refactoring step of TDD. Those tests alone (as you have seen) aren't going to cut it and there's nothing that tries to "add apples" more than once to find out if it's even doing it's job incrementing the total(even though we can see that). The tests provided, I could have just set _totalAppleCount instead of incrementing it and the tests would still pass. For me, that was literally the first two passes at the class. There would be more and it would drive out the desired behavior.

That being said, I don't object to having individual cases and that's certainly where the TDDing starts for me. The test cases may be factored out down the road to have a single test for a variety of inputs. I wouldn't dictate that one way or the other is the best place to start. It's up to you.