"How do you deal with Legacy code that lacks test coverage?" - even miniscule small changes are hazardous, and often, a necessary rewrite is postponed forever because it's such a nightmare to work with. Even if you have invested time into your test coverage after taking over the system, chances are there are still parts of the system you need to deal with that aren't covered at all. So this is what I propose in this situation:
Test Cocooning is a reversed TDD cycle, and it should be common sense. |
The Cocooning process
- Based on what you think the code does, you create a cocooning test.
- If the test fails, you didn't understand the code correctly and you have to improve your test.
- If the test passes, you have covered a section of the code with a test that ensures you don't accidentally break the tested aspect of the code.
- Based on what you think the code does, you make a breaking change.
- If the test fails in the way you thought it would, you have a correct understanding of that piece of code.
- If the test passes, you didn't understand the code correctly and you have to improve your test (back to step 1)
- Intermediate activity: Of course, you revert the change to restore the behaviour that you have covered with test.
- Within the scope of your passing test, you begin to improve:
- Create lower-levelled tests that deal with more specifics of the tested code (e.g. unit tests.)
- Refactor based on the continuous and recurrent execution of all the relevant tests.
- Refactor your tests as well.
- Re-run the original cocooning test to ensure you didn't mess up anywhere!
Once a cocooning cycle is completed, you should have reworked a small section of your Legacy code to be Clean(er) Code that is more workable for change.
Iterating
You may need to complete multiple cocooning cycles until you have a sufficient amount of certainty that you can work on the code reliably.
Backtracking
The important secret of successful Test Cocooning is that you need to backtrack both on the code and your tests - after completing all relevant cocooning cycles, you'll need to re-run:
- your cocooning tests against the original legacy code.
- your unrefactored original cocooning tests against the new code.
- your unrefactored original cocooning tests against the original legacy code.
Working Cocooned code
Bugfixes
- Create a test cocoon for the current behaviour which passes under the current faulty(!) conditions of the code segment that exactly reproduces the bug as though it were desired behaviour.
- Create a test which fails due to the bug, i.e. add a second test that exactly reverses the cocooned behaviour.
- Write the code that meets the requirement of the failing test.
- As a consequence, the cocooned passing test for the bug should now fail.
- Ensure that no other tests have failed.
- If another test has failed, ensure that this is intentional.
- Eliminate the broken cocoon test that reproduces the bug's behaviour.
- If there were other tests that failed, now is the time to modify these tests one by one.
- Backtrack like described above to ensure that nothing slipped.
Modifying features
New functionality
Rewrite
Closing remarks
- I believe that test cocooning requires both strong test and development expertise, so if you have different specialists on your team, I would highly recommend to build the cocoon in pairing.
- Cocoon tests are often inefficient and have poor performance. You do not need to add these tests to your CI/CD pipeline. What you must add to your pipeline is the lower-level tests that replicate the unit behaviour of the cocoon. It's totally sufficient to rerun the cocoon tests when you work on the cocooned Legacy segment.
- Cocooning is a workaround for low-quality code. When time permits, rewrite it with Clean Code and you can discard the cocoon along with the deleted code.
- Do not work on Legacy Code without a solid Cocoon. The risk outweighs the effort.
No comments:
Post a Comment