Fix bugs on legacy code
Blog post series
When we need to fix bugs on legacy code, we first need to understand if the described behaviour is in fact a bug or not. For that we can write some characterization tests in order to understand what the system really does. The simplest form of characterization test is a system test. A couple of ideas to start writing the characterization tests are to use the generic approach Part 2 – From Nothing to System Tests and Part 3 – Golden Master. We can generate system tests considering that the System Under Test (SUT) is a black box. You can find more details about how to do that in the blog posts and code casts about the above techniques. But in order to fix bugs on legacy code we need to dive more into the code base. We need to write tests on a smaller scope and we often need to refactor in order to make room for the code changes. Let’s see a technique of fixing a bug in legacy code.
Here are some steps in order to fix bugs on legacy code in a safe way by taking baby steps. We need to focus on the problem and not be distracted by messy code that is around the area where we would need to fix the bug. If the code is messy, but it does not contain bugs we need to let it be. The most we can do is take a note about the problems and add this item into a technical improvement backlog that you can discuss later with your colleagues.
Important: commit after each step to a local source control
Why would you want to commit so often? Well you are working with a system that could be very hard to change and very hard to understand. Mistakes can come out from any small change. You want to have a very easy to use undo button with all the changes. You would not like to be in the situation where some tests stop passing and you do not know what you did. Always when something is wrong, you can undo and continue from a clear state. This is a kind of backtracking in a maze of possible decisions.
- Find the bug by writing a black box characterization test
We always need to wonder: is this a bug? And we need to figure the real behaviour of the system. The way I am proposing now is to start writing a system test that would be a safety net. But often this is not enough to reach that certain bug. So we often need to go deeper with smaller characterization tests that might require us to refactor the SUT. So make sure you have a safety net before starting step 2.
- Refactor the SUT in order to make room for smaller scope characterization tests
We need to start refactoring like we would want to dig out for a precious stone. We need to carefully find the area of the code where the buggy code is and isolate it. In this case we need to use legacy code techniques to break the dependencies. When the code is ready we start writing one or more characterization tests in isolation.
- Write characterization tests in isolation
We need to write as many smaller, isolated characterization tests as we need until we understand if the alleged bug is in fact a bug. When we have found the real behaviour of the system we have mainly two options: it really is a bug or it is in fact an ill-reported bug. In the latter case nothing more can be done than to report the status. But in the former case we need to fix the bug.
- Write a test that would express the correct behaviour
We need to write a test that will show what the correct behaviour is. This test will be red, and it is normal to be red.
- Refactor the system to accept the changes
During this step we need to make room for the change. Sometimes this step is not needed, but sometimes we need to take some baby steps in order to make sure the implementation step will not be very long and in consequence defect-creator prone. The previous test had helped us understand what are the needed changes to the system. Of course we need to have covered by tests all the code that needs to be refactored.
- Make changes to the SUT to make the new test pass
After the prior refactoring step this step should be straight forward. We should make some small changes and the new test expressing the correct behaviour should pass from red to green. The old test however should pass from green to red. In this moment we can say that we fixed the bug.
- Refactor, clean-up
This is the last step of the process. We need to delete the old test which is now red, delete the unused code and of course we need to leave the camp clean by refactoring the production and the test code.
Remember: commit after each step to a local source control
After this session we will have an idea on how to fix bugs on legacy code. If you apply the steps rigorously you will spend less time fiddling with the code, and you will have more certainty that you had not introduced a new bug and that you actually fixed the bug.
You need to have a lot of imagination when applying this technique. During the parts when you need to find the good characterization tests and refactor in order to dig inside the code base so you would have room for making the changes you will need to be very creative. I do recommend a lot of practice with smaller code bases or projects before starting this on a big ball of mud project.
I came up with this technique in the struggles to improve existing code without breaking it. Taking smaller and smaller refactoring steps helped me a lot to diminish the risks to almost zero. Also during some of my struggles to solve problems on existing code I thought about minimizing my commit time and invented the Taking Baby Steps game. Then I realized that taking very small steps where the options are clear and the risks are manageable is essential when working with existing code.
Check here a code cast in Java about this session.
Feel free to contact me to find more about how to fix bugs on legacy code.
Image credit: http://openclipart.org/detail/192938/heart-bleed-patch-3-by-merlin2525-192938