While at SD West I had the chance to attend a talk by Elliotte Rusty Harold who has written numerous books such as Refactoring HTML just to give an example. This talk was a tutorial on writing tests for existing production legacy code. Elliotte has a very practical approach to dealing with adding tests to existing legacy code.
The benefits of TDD are clear and 100% code coverage is always the goal. However, with existing legacy code you need to take a much different approach. When optimizing, or refactoring existing legacy code you can apply TDD and it works very well, however you must give up a few things:
- 100% Code Coverage
- Unit Tests
Focus on Broad Tests
Since you are writing tests for a system that has already been running on production for quite some time, you can assume for the most part it is offering what the user’s want, or there would have already been complaints and issues logged (there will be bugs, but overall it is functioning). So don’t focus on 100% code coverage or unit tests, instead focus on writing broad tests that confirm some functionality that the user relies on is working correctly.
Yes, when you make changes and a broad test does fail, it will not immediately show you the cause of the failure, like a typical unit test will. However, it will still show you that you have broken existing functionality and this is a big improvement over having no tests at all. In a small amount of tests we have been able to at the very least protect some existing functionality. The key is that since we usually have a very limited amount of time to devote to writing tests for legacy code, that it is more important to get broad coverage than targeted coverage.
Just like when using TDD with new functionality, when making any change at all in legacy code, ensure the tests are written BEFORE changing anything, but still in this case broad tests are the place to start. This is not advocating ignoring full coverage for your new code, only the legacy code. If you have time to write full unit tests for existing legacy code, great, but most developers do not. The key is to make sure your broad tests are in place FIRST.
When dealing with legacy code, the fastest gains in code coverage and also the biggest gains in reducing bugs come from starting your tests at the highest level possible. In writing these tests you do not think about the structure of the code, but rather just “What does this application do” and write tests to confirm that functionality and protect it.
Add a New Test and It Fails
Now in TDD when writing new code, the moment a test fails you fix the code until your test passes. However, when writing tests for existing legacy code, if you add a new test to an area that is not currently covered by tests and it fails, but you are confident the test should work, do not stop and fix the code to get the test to pass, this is a common mistake. You are far better off to log an issue for the bug and to continue to write more tests. The reason is because at this stage you do not have enough tests in place to ensure your bug fix will not break another area of the application. So write more tests, leave this test failing, and log an issue for fixing this bug.
Basic Method to Testing Legacy Code
So to get started with writing tests for legacy code you:
- Look at the existing application
- See what functionality it offers to the user
- Write tests to cover that high level functionality
Top Down Approach
When writing tests for legacy code, you want to go in the order of what will give you the most gain for the least amount of effort, so go in the following order:
- Tests for each Package
- Tests for each Class
- Tests for each Method (with legacy code, will probably never get this far)
- Tests for each Line (100% code coverage)
Once again the Michael Feathers book Working Effectively with Legacy Code was highly recommended. When I read this book I enjoyed it a great deal. At the conference this book was recommended at most talks I attended and is highly regarded as a reference for refactoring and getting code into a testable state.