Moving existing code to Test Driven Development

Working Effectively with Legacy Code is my bible when it comes to migrating code without tests into a unit-tested environment, and it also provides a lot of insight into what makes code easy to test and how to test it.

I also found Test Driven Development by Example and Pragmatic Unit Testing: in C# with NUnit to be a decent introduction to unit testing in that environment.

One simple approach to starting TDD is to start writing tests first from this day forward and make sure that whenever you need to touch your existing (un-unit-tested) code, you write passing tests that verify existing behavior of the system before you change it so that you can re-run those tests after to increase your confidence that you haven't broken anything.


I call it "Test Driven Reverse Engineering".

Start "at the bottom" -- each class can be separately examined and a test written for it. When in doubt, guess.

When you're doing ordinary TDD in the forward direction, you treat the test as sacred and assume that the code is probably broken. Sometimes the test is wrong, but your starting-off position is that it's the code.

When you're doing TDRE, the code is sacred -- until you can prove that the code has a long-standing bug. In the reverse case, you write tests around the code, tweaking the tests until they work and claim the code works.

Then, you can dig into the bad code. Some bad cade will have sensible test cases -- this just needs to be cleaned up. Some bad code, however, will also have a test case that's senseless. This may be a bug, or clumsy design that you may be able to rectify.

To judge if the code's actually wrong, you also need to start at the top with overall test cases. Live data that actually works is a start. Also, live data that produces any of the known bugs, also a good place to start.

I've written little code generators to turn live data into unittest cases. That way, I have a consistent basis for testing and refactoring.


See the book Working Effectively with Legacy Code by Michael Feathers.

In summary, it's a lot of work to refactor existing code into testable and tested code; Sometimes it's too much work to be practical. It depends on how large the codebase is, and how much the various classes and functions depend upon each other.

Refactoring without tests will introduce changes in behaviour (i.e. bugs). And purists will say it's not really refactoring because of the lack of tests to check that the behaviour doesn't change.

Rather than adding test across the board to your whole application at once, add tests when you work in an area of code. Most likely you'll have to return to these "hotspots" again.

Add tests from the bottom up: test little, independent classes and functions for correctness.

Add tests from the top down: Test whole subsystems as black boxes to see if their behaviour changes with changes in code. And so you can step through them to find out what's going on. This approach will probably get you the most benefit.

Don't be too concerned at first with what the "correct" behaviour is while you are adding tests, look to detect and avoid changes in behaviour. Large, untested systems often have internal behaviours that may seem incorrect, but that other parts of the system depend on.

Think about isolating dependencies such as database, filesystem, network, so that they can be swapped out for mock data providers during testing.

If the program doesn't have internal interfaces, lines which define the boundary between one subsystem/layer and another, then you may have to try to introduce these, and test at them.

Also, automatic mocking frameworks like Rhinomocks or Moq might help mock existing classes here. I haven't really found the need for them in code designed for testability.

Tags:

C#

Nunit

Tdd