TDD when up to your neck in Legacy Code

So you are working with Legacy Code and want to deliver new features on time and without introducing new bugs; what are your options?

Edit and Pray

Up To Your NeckIf you are not writing unit tests then you are doing Edit and Pray. I spent the first 5 years of my career as a programmer perfecting Edit and Pray. I thought of it as working carefully. I didn’t think of it as me making a change and then just hoping that nothing bad had happened. I learned as much as I could about the system so as to be able to make a good guess about the possible consequences of any changes. Then I painstakingly double- and triple-checked everything before crossing my fingers and deploying. And most of the time it worked but new members on the team had to spend a few months learning the code base before they could make any significant changes.

Cover and Modify

A much better option is to write tests to cover the legacy code that you want to change and make a safety net for it. This is the first step to isolating it from the rest of the code and stopping any changes rippling through the system. Tests written to cover code can be quite different from unit testing and from testing at the GUI level. Their role is to be the canary in the coal mine; they detect if any changes to the code have modified the existing behaviour of the code under test.

Once these covering tests are in place, then the process of refactoring and breaking the code up into units suitable to be unit tested can begin. Later on when you are confident that the code under test is now covered with proper unit tests then the covering tests can be deleted.

Why Unit Test?

If you are lucky then you might have a few large integration tests for the critical parts of your system. For example, you might have a test, taking 5 –15 minutes to run, that initialises test data and then executes all the invoicing logic and checks the result afterwards. A test like this has value but the feedback is very coarse-grained. You won’t be running this after every change in the code and if it throws an exception or returns an incorrect result it might take a few minutes to find out why (or longer if you have to debug it).

Unit tests give you instant feedback at a very local level. They can be run after every change you make and it takes seconds to find any error. This is vital to allowing a programmer to continuously refactor with confidence. I can make a small change to improve the code and know almost instantly if I have broken anything. And in contrast to practising Edit and Pray, a new programmer can come in and change code and feel confident that they have not broken anything important. The learning curve will be much easier as well due to the unit tests acting as living documentation.

TDD and Legacy Code is too hard

A lot of programmers might agree that introducing unit tests into legacy code would be very helpful but think that the process is too hard or time consuming. I can’t count the number of times I have heard someone lamenting that they would love to learn TDD but can’t because they are working with legacy code. Everything is hard if you don’t know how. There are techniques to introduce tests that any programmer can learn and these techniques belong to the core of TDD and good code design. If you learn how to unit test legacy code, then you are on the path to mastering TDD and becoming a much better programmer.

The great thing about TDD is that it encourages taking small, baby steps and this is perfect for working with legacy code. You do not want to make big changes and risk introducing unnecessary bugs. As you begin to master the techniques of refactoring legacy code, you will nearly always be able to find a way to progress with a new baby step.

But first…

Stop writing legacy code. Do not write new features without unit testing. You are just making the code base worse and getting further away from introducing tests into your system. Get your testing infrastructure set up and a few tests running successfully and it will be much easier to think about introducing tests for the rest of the code.

Sprout Method

When  adding new functionality, write the code in a new method and with TDD and then call this method from the old code. So even if you can’t test the code where your method is being called, at least the new code has tests. This technique (from Michael Feathers again) is called Sprout Method.

I use this technique all the time when I am working in an ASP.NET Web Forms project. As it is very difficult to test the code-behind, I write all new functionality in a separate class that is then called by the code-behind class (in Page Load or a button click event handler etc.)

Wrap Method

Another technique to introduce new tested code is Michael Feathers’ Wrap Method pattern. It works with new code that is called directly before or after the old code. Adding a new logging method is the typical example.

Here we have an a method for sending an invoice that carries out lots of different, complicated tasks. I don’t really understand all this code yet but I need to log that the invoice was sent.

public void SendInvoice()
	//Code to get invoice data
	//Code to send invoice by email

There are two steps to this method:

  1. Extract all the code in SendInvoice into a private method which I call SendInvoiceToCustomer.
  2. Call SendInvoiceToCustomer and my new LogInvoiceSent method (created using TDD) from the SendInvoice method.
public void SendInvoice()

Now existing code will call SendInvoice in the same way as before but now will also be logged. I haven’t changed the logic or flow of the original SendInvoice method and therefore haven’t risked introducing a bug. However, this technique will only work for code that can be called either before or after the original method.

There are other related patterns (Sprout class and Wrap class) and variations in Michael Feather’s book Working Effectively with Legacy Code.

Next Step

Now all new code is produced with TDD but we are still making untested changes in the old code when we call our new methods. After you have got to know the system a little better it is time to take more baby steps. Next baby step: breaking hidden dependencies.

Leave a Reply

Please log in using one of these methods to post your comment: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s