How to deal with legacy code
A few techniques to deal with legacy code:
Breaking dependencies using a Seam
Safe refactoring
Extracting methods
Extracting classes
Abstracting libraries
Create characterization tests (gold standard tests) before refactoring
1. Breaking dependencies using a Seam
Sometimes it is not possible to write tests due to excessive coupling. If your code is dealing with external dependencies, we can break the dependency to allow the Subject Under Test to be exercised independently.
In clothing, a seam joins parts together to form a piece of clothing. In code, we can use this concept to find soft points where we can separate coupled parts.
Ideally, we want most of the changes on test code and not on production code.
An example of a Seam
Using Inheritance to Decouple Production Code
In this case, the Game class is coupled with the random number generator library. We need to control the dice rolling in order to test the Game class.
Step 1
Add a protected virtual method to the Game class to encapsulate the behavior that has the coupling issue
Step 2
In your test code, inherit from the Game class and change the behavior of the protected virtual method Roll to something you have control over:
Step 3
Write a test using the TestableGame
class:
Advantage
The advantage of using this method is that it minimizes changes to production code and can be done using automated refactoring, thus minimizing the risk of introducing breaking changes to production code
2. Safe refactoring
The term refactoring is often used incorrectly. When refactoring, you are merely changing the structure of the code. If the logic and/or signature of the code in question changes, then this does not qualify as refactoring!
If I'm changing the structure of the code (refactoring), then I don't ever change its behavior at the same time. If I'm changing the interface by which some logic is invoked, I never change the logic itself at the same time.
– Kent Beck
3. Extracting a method
Working with legacy code often involves working with very large methods. A large method is any method that is longer than twenty lines. A large method can mean that the code is violating the Single Responsibility Principle.
What needs to be done is to find the seams in the method. Seams can be found by adding comments to describe different sections of the method. Once you have added the comments, you have identified the seams.
Each one of those seams is probably a lower order method that can be extracted. In most editors and IDEs, highlight the code you want and use the extract method refactoring provided through either a right-click, context, menu, or via the menu bar.
4. Extracting a class
Just like methods, sometimes a large method should really be a class. While extracting methods, if you extract three or more methods, then you have probably found a class that needs to be extracted! Extracting a class is similar to extracting a method and is likely supported by your editor or IDE.
5. Abstracting libraries
Things like DateTime, Random, and Console are best hidden behind classes that you design to fit the needs of your application. There are several reasons for this;
Most importantly, putting these in their own classes will allow for testing. Without abstracting these to a separate class, it is almost impossible to test with things like DateTime that change values on their own.
6. Characterization tests
Gold standard tests, or characterization tests, are those tests that simply define the expected functionality of a method. If you were to add tests to a legacy system, you would likely begin by writing gold standard tests to define the "happy path" through the system.
You might run the application to determine what values a given method returns based on a given input, and then write a test to duplicate the results.
For these Gold standard tests, You certainly don't want to run the application with all possible values in order to write tests for each of the possibilities. It is far more important to test for every path of execution.
Last updated