Friday, September 19, 2008

TDD is a Design Activity

This has been written and talked about before, and if you've read Kent Beck's TDD book, you'll understand how much it's emphasized, but I still see a lot of test code that is about validation and very little about design. And it doesn't need to be that way. Simply follow the three rules of TDD:

  1. Only write production code that makes a failing unit test pass.

  2. Write the minimum amount of test code to get production code to fail. Not compiling counts.

  3. Write only enough production code to get a test to pass.


What I see the most from the test code I read is violations of rule two. Which is very understandable. Why would you not want to write more test code? Because tests, just like every other piece of the system, have three functions:

  1. implement the desired feature

  2. afford change

  3. reveal intent


Writing line after line of test code to cover every possible corner condition certainly meets the first, but violates the second and third. I've seen test code that looks something like:

public void testServicesStopped() {
assertServicesStoppedIfEnabled(false, false, false, false, false, false);
assertServicesStoppedIfEnabled(true, false, false, false, false, false);
assertServicesStoppedIfEnabled(false, true, false, false, false, false);
assertServicesStoppedIfEnabled(false, false, true, false, false, false);
assertServicesStoppedIfEnabled(false, false, false, true, false, false);
assertServicesStoppedIfEnabled(true, true, false, false, false, false);
assertServicesStoppedIfEnabled(false, true, true, false, false, false);
assertServicesStoppedIfEnabled(false, false, true, true, false, false);
assertServicesStoppedIfEnabled(true, true, true, false, false, false);
assertServicesStoppedIfEnabled(false, true, true, true, false, false);
assertServicesStoppedIfEnabled(true, true, true, true, false, false);
// every possible combination was covered
}

Certainly a very thorough test, but also very very intention hiding. Unit tests should function as documentation for the system. You should be able to read the test and determine what a module does and how it does it. To make matters worse, testServicesStopped() was the first method in the test and assertServicesStoppedIfenabled() was near the bottom with many other tests and helper methods in between. To try and figure out what each boolean meant, you had to scroll up and down the file and try to remember 6 parameters. I would also bet a considerable sum that the bulk of this code was written after the production code was completely finished. Not only does this test code not reveal intent, but it's very rigid. It's hard to change because it's hard to understand. When the customer wants to change how this feature works, and the developer opens this test to write the failing test so he can write the feature changes, he'll have a very difficult time figuring out what to change.

Having a suite of unit tests you can run to verify your system still functions is very important, but just as important is having those unit tests clearly document the system. The easiest way to achieve this is to use TDD the way it was designed to be used by following the Three Laws of TDD.

No comments: