To me, legacy code is simply code without tests.
— Michael C. Feathers, author ‘Working Effectively with Legacy Code’
Most of us will work with this type of legacy code at some stage. Trying to add tests can be very difficult. If the code has been written without tests, then chances are it is tightly coupled with other parts of the system. In order to work effectively and produce robust solutions we need to have a few techniques in our arsenal to make the code testable. After reading ‘The Art of Unit Testing’ by Roy Osherove I’ve been experimenting with the techniques recommended in the book. Below, I’ll work through the 3 techniques I use most regularly.
Here we have an example of the kind of tight coupling you might encounter in a legacy application. The ReportGenerator
is used to fetch a block of text containing a user’s name, their results, and the date the report was generated.
The output will look something like this:
Joe BloggsA,B,A,CDate: 29/03/2017
The class contains 3 tight couplings affecting our ability to write tests. UserService
and ResultService
both make requests to our database which we must be able to mock. The call to DateTime.Now
also affects our ability to write tests as it will return a different string every time it is called.
Instead of constructing the Services within our GetUserReport
method we can pass in interfaces via the constructor. First we need to extract interfaces for our Services (with a bit of resharper magic…):
internal interface IUserService {User GetUser(int userId);}
internal interface IResultService {string[] GetUserResults(int userId);}
Then modify the ResultGenerator
to have its dependencies injected:
Now we are able to mock our Services and test the ResultGenerator
. This is a common way to achieve of decoupling and is often recommended in clean coding guides and discussions on the SOLID principles.
Whilst this solves our coupling problem, it sometimes results in an enormous amount of work. What if our result generator is called from many different locations? Then we must create instances of our services in every file where it is used; suddenly we have gone from writing a simple test to modifying a large number of files. Also, if there are many dependencies in the class, the constructor can become unwieldy due to an ever-growing list of parameters. This technique hasn’t helped us break the dependency on our system call to DateTime.Now
.
To minimise the impact of introducing tests to the ResultGenerator
we may wish to avoid modifying the constructor. We can still achieve the ability to provide mock implementations by wrapping our service calls in CLR properties and lazy loading the implementation.
With this technique we can leave most of our code as it is and simply override the service implementations in our test code.
This solution is preferable where we want to confine the scope of our changes. However it still doesn’t help us with the DateTime
problem.
This is a clever approach recommended in The Art of Unit Testing. It is similar to Technique 2, but also addresses our need to mock the system call to DateTime
. If we wrap our dependencies in protected virtual methods we can provide mocks by creating a derived TestableResultGenerator
which overrides these virtual methods:
Then we create a TestableResultGenerator
in our test code which derives from ResultGenerator
and overrides the available virtual methods:
This testable implementation allows us to provide mock implementations in our tests.
This is somewhat controversial since we aren’t directly accessing the code under test, but through a proxy.
Decoupling legacy code can be a difficult problem. It requires a significant amount of upfront work for little immediate benefit. This can be an especially hard sell to management when the pressure is on to meet deadlines; however the increased quality and maintainability of the code base has very tangible benefits in the long run.
Mastering these techniques will ease the process of decoupling. Using combinations of these techniques will provide elegant solutions to most problems you will encounter.