Saturday, January 07, 2006

Adding Unit Tests to Legacy Code

I've started adding Unit Tests to a "legacy" code library. So far, the basic approach I am taking is:

  1. Create the test harness.
  2. The first test is to compile the old code library. Of course that fails at first because of all the things the library depends on.
  3. Create enough "fake" class files that the library compiles.
  4. Pick one subroutine (method) to test, and add a test that runs that subroutine. Of course it fails because of all the missing dependencies - the subroutine under test uses a bunch of subroutines defined in others.
  5. In the test suite, create a FakeMethods class that defines stub versions of the missing external methods/subroutines, for example, the subroutine I am testing calls GetTotalAmount($cost,@items) so in the test suite I have something like this:

    sub GetTotalAmount {
    cluck('fake sub called'); # Print a stack trace showing we were called
    my($cost,@items) = @_; # Document the arguments expected
    return; # Return nothing for now
    }

  6. Some of the fake subroutines will need to return some actual values for the subroutine I am testing to run. Add just enough input so the test passes, even when this seems ridiculous. For example, I found that one subroutine wanted the name of a file to open, and that the subroutine would run even if I passed in the name of a non-existent file. OK, that's what I did. Later, we can add a test expecting the subroutine to throw an exception if the file doesn't exist, and then add defensive code to throw the subroutine.
  7. Continue repeating steps 5 and 6 until the subroutine I am testing runs without throwing an exception.
  8. Add tests to run the subroutine with variations on its arguments and/or environment. I may need to add more fake subroutines, mock data, etc.
  9. The result is that I have a pretty clearly documented view of what the subroutine/method actually requires to run as of today.

2 comments:

Luke Closs said...

After you flesh out a bunch of tests, you could use Devel::Cover to find areas poorly tested and dead code.

Matisse Enzer said...

That's a good point.

In the module I'm working on now, there are around 4000 lines of code and dozen of subroutines - just finding dead code would be a help.