Code has bugs. Finding a bug’s hiding place is a challenge. And, you know that killing a bug often breaks code in unexpected ways, hatching more bugs to discover, hunt down, and kill.
If you created your whole code base using TDD, you could prevent many of these new bugs. But you have legacy code; code without tests. How should the professional software Orkinman apply DDT, I mean TDD, to bugs in existing code. (Orkin (r), do i have to do this in a blog?)
If the Orkinman is fortunate enough to have some tests already in place, the job is much easier. But if not, it is a great opportunity to get some automated tests probing your application. You know bugs hide together, so the tests you write just might help track down or prevent other bugs.
So, you have a bug to fix. Let’s say you can manually reproduce the bug. The next thing to do is to write a test that revels the bug. This is often easy to say but hard to do. The bug is well hidden, so you don’t know what to unit test. You do have an alternative. Before the bug hunt, write a functional or acceptance test that reveals and confirms the bug.
When you have the failing test written, writing additional functional tests gets a lot easier. You have the structure in place, so now its a matter of cut, paste, tweaking the inputs and outputs. Choose test data that exercises the code in subtly different ways. Probe the boundaries of the bug; solidifying your understanding of what is broken and what works.
Refactor your test code after every cut and paste. If you don’t, your tests will be fragile and hard to understand. If you do, the differences between test cases will stand out. Your test cases will be easier to understand, and the next test case will be built out of the common pieces extracted from prior test cases.
Hopefully all these new probing test cases pass, but you might just get surprised and find some more bugs while you are at it. As you investigate the scope of the bug, you learn if there is just one bug or an infestation. You invest in your future; getting a few other tests in place around the area you are about to change might just warn you when your fix breaks opens a new nest.
With tests in place, the hunt begins. In addition to your usual bug tracking techniques, you now have a test fixture created that lets you run controlled experiments on the code. When you learn something new along the way, write a test to capture and confirm what you suspect. You’ll make some assumptions; capture them in the tests. The tests confirm your understanding.
Don’t kill the bug as soon as it is backed into the corner. Write the unit test that reveals the bug. Using your judgment, write a few tests that show the proper behavior of the code you are fixing. You don’t want collateral damage when you kill the bug.
Finally! Fix the bug!! See all tests pass!
This may sound like a lot of work, and it will be initially. But it will get easier with experience and easier as your test case base grows.
Some things to remember:
The first test in a new area is the hardest to write. If you refactor the new tests while they are fresh in your mind, you will have test building blocks that will make the next tests easier to write. The tests preserve the knowledge acquired while debugging. Many of the activities needed to track down the bug are learning activities. Writing the tests, as you learn, captures knowledge in a useful form that for yourself and others.
As you hunt the bug, or after you find the bug, it is a good time to look for other vulnerabilities in the code. Bugs tend to hide together. While all this is fresh in your mind, adding new tests is easier and cheaper than it will ever be again. The tests may reveal bugs immediately, or they may provide a warning to avoid future collateral damage.