Thursday, December 27, 2007

Unit Testing and TDD

Unit Testing and Test Driven Development have been around for quite awhile. However, they're still just beginning to really become a commonly seen practice in industry programming, partly because of the influence of Agile Programming and Dynamic Languages.

A unit test is very simple (and it has nothing to do with any manor of human body part). From a code perspective a unit is a public class with public properties and methods. From a design perspective a unit is a "high level" function or behavior or task or object. A unit test is a method that tests a certain behavior of the unit. Thus one unit will have many unit tests, each one testing a different part of its behavior.

Sometimes unit tests are written by "QA" people after the developers have finished writing the code. Other times, unit tests are written by the developers themselves after the code has been written. Both of these approaches are terribly boring, inefficient, and ineffective.

Test Driven Development is the practice of writing unit tests BEFORE production code. It goes like this:
  1. Write a list of things to test
  2. Pick a test and write it before you've written the production code
  3. Write the minimum amount of code needed to pass the test
  4. Refactor the production code AND test code, if needed
  5. Repeat

This is typically referred to as the Red/Green/Refactor pattern. It’s very festive and Christmas appropriate. First, you write up a list of tests. Then you decide what test you want to code first. Being a lazy computer geek who would rather IM the guy in the other room than actually get up out of your chair and (*gasp*) walk, you pick the easiest test first. Since you haven't written the code that the test is supposed to test, your test probably won’t compile. So, you write the minimum amount of code needed to get it to compile by defining the class you invented out of thin air in the test and any properties or methods you may have whimsically pretended it had. Now you run the test and it fails since none of those methods or properties actually do anything yet (red). Now that you see red, you can actually write some code. You write the minimum amount of code needed to get the test to pass (green). Then you survey your code and your tests and refactor as needed, re-running the tests when you're done. At this point you enhance your list of things to test, in case you thought of something new, and pick another test.

Every line of code you write is directly driven by a unit test (or a refactoring): Test Driven Development.

If you think I’m over emphasizing the minimum concept above, wait until you read your first TDD code sample. The first one I saw tested a Queue’s IsEmpty method. The test was that when the Queue is first created, IsEmpty returns true.The code they wrote to pass this test was:
public bool IsEmpty()
{
return true;
}

So when TDD people say do the minimum, they really mean it.

It sounds quite simple and for the most part it is. However, there are some guidelines which will help make the unit tests more useful.
  1. Tests must be fully automated requiring no external input or setup that is not performed by the tests themselves.
  2. Don't write code unless you have a failing test. Your tests define the code's requirements.
  3. Each test should test as little as possible. That is, be as specific as possible and test exactly one feature of the unit. Then, when a test fails, you know exactly what is wrong.
  4. Tests should exercise only the unit being tested and not any of that unit's dependencies (ex: database, other units, etc). This is accomplished through dependency injection and mock objects.
  5. Test code is not a secondary citizen; it is just as important as production code and should be treated as such.


The main question to ask about Unit Testing and TDD is: does it help? Well, help with what? Bugs? Time to develop? Maintenance? Code design?

Here are some of the pros I’ve seen while doing TDD:
  1. Fewer bugs: Thinking through what tests to write, and actually writing them, frequently reveals other test scenarios I would have missed. This is partly because I’m not doing "gunslinger" development and partly because you are using your classes before you’ve written them.
  2. Regression testing: Worried that code you wrote three months ago still works? Run the tests. Worried you forgot something during your code refactoring? Run the tests. Worried you broke your public interface by accident? Run the tests.
  3. Easier debugging: The test tells you exactly what is wrong and you can frequently tell exactly what LINE of code is wrong.
  4. Improved code design: Because you need to test one unit at a time, you naturally design your code into smaller, “DRY”-er, more orthogonal, and more composable units. You also tend to design around algorithms instead of UIs.
  5. Increased confidence: Because everything is tested and tests are fast and easy to run, you KNOW your code works. When your boss asks, "Does this work?" you don’t have to say, "Last time I checked..." The last time you checked was immediately after your last build. "It works."
  6. One development style: Whether you’re writing new code, or prototyping, or debugging, or adding features, or working on someone else’s code, it’s the same process: Red/Green/Refactor.
  7. Feeling of "making progress": You’re constantly finishing small units of work in the form of tests and getting instant feedback that the work is correct. This means you don’t have to go days hacking on some deeply embedded code, hoping you’re getting it right.

What are the cons?
  1. At least twice as much code: Tests require lots of code. You have to write it, there is no avoiding it.
  2. Tests take time: Writing all those tests takes time.
  3. Test Maintenance: You have to keep that test code up to date if you want it to be worth anything.
  4. Dependency Injection can get ugly and it can be hard to know where to draw the line, as I discussed earlier.
  5. Some things can’t be tested: You can’t test GUIs. There are also things that are just too hard to test. Asynchronous timing issues for example.
  6. Code has to written to be tested: You can’t test any arbitrary code. You can test parts of it in some cases, if you’re willing to deal with dependencies it may have. But to test it "properly" it has to be written with testing in mind.

So is it worth it? Do the pros out weigh the cons? So far, I think they do. I even think that in the long run, writing unit tests may actually save time because they make it so much easier to update code and verify that code is working correctly months and years later. They also make it easier for multiple developers to work on the same code at different times. Also, if you’re doing proper TDD, then maintaining the tests isn’t an issue. And you can address old code that isn’t tested by refactoring it and testing the refactored portions little by little as you fix bugs and add features. And while you may not be able to click a button on a form, you can call the method that the button’s click event handler would have called.

All that being said this is still computer programming.There is no silver bullet. But TDD seems to get between 70% and 80% of the way there, and that’s certainly better than randomly clicking around your Forms...

Finally, if you're completely new to this stuff you're going to want to check out nunit for unit testing and nmock or Rhino.Mocks for creating mock objects, all of which are free.

And now we’ve reached the part of the post where I ask what you think about Unit Testing and TDD. Do you agree with my take on it? Do you have stuff to add?

1 comment:

  1. As always a great topic and i have a couple comments.


    1) I believe in TDD but i havent seen it be a benifit in my day to day job. We did it for a couple months and then moved on to development driven testing (which as you mentioned is a waste of time). I also have issues that i think on an architectural level our app isnt made to be tested using TDD. As you said you have to write testable code and in many ways that requires a testable architecture.

    2) There are a LOT of projects to bring TDD to the UI. I forget the names of them but i have some somewhere in notes for the chicago conference i went to. Basically you test UI standards and whatnot.

    3) Mocking is a odd topic in the TDD world. You are saying Unit testing which yes is limited to the unit but some people believe in testing units in a heirarchical approach. So you test the lowest unit then if that is tested well you can assume that higher unit tests will only test the higher unit and their interaction, both of which is good to test.

    ReplyDelete