Take your time
If you rush, you might miss some implications of your changes and cause more work in the long run. So, do it right the first time.
Secondly, since you’ll be in the code anyway, take some time to ensure that it will survive unchanged for another stretch of time.
Estimate appropriately
It’s difficult to estimate how long a change to code you’re unfamiliar with might take. For legacy code, even more so. Make it clear, however, that this is a learning and improvement expedition, not just a ✅ ticket.
Ensure and improve test coverage
Knowing that sufficient and necessary coverage exists to give you the confidence to make your changes is a hard pre-requisite.
Investigate the existing tests
First, find all existing tests to determine whether there is sufficient coverage. If you use a tool like Coveralls or one that selects a subset of tests to run for a PR, you can query it to get a list of tests that cover the feature.
You can also change the feature to throw an error and re-run the test suite to see which tests will fail.
This step will also help you understand the code and its dependencies.
Improve coverage
Next, improve coverage until you’re confident that any breaking change will be caught. Keep in mind that tests must be sufficient and necessary.
In my experience, not only will you need to add test cases but you might need to refactor the existing test suites.
As code evolves, new test cases are just added to the bottom of the test.
The test file then consists of one-half of nicely structured test cases, possibly broken down by function or branch.
And of one-half of test cases in chronological order.
Ensure the correct mix of test types
As you’re improving coverage, you should also pay attention to the proportion of the different test types.
Changes in related code often mean that tests are changed or removed. Over time, the proportion of unit/integration/end-to-end tests is likely to go out of balance.
Updates to the testing guidelines might also call for changes in test proportions.
❗ At this point, you should have well-tested, but still legacy code.
Refactor the legacy code
Since you now have a nicely tested piece of code it should be easy to employ the Scout rule and leave the place better than you found it.
Ensure that the legacy code complies with current guidelines
Legacy code that is changed only rarely will not get upgraded to the latest company guidelines.
For instance, if your organization adopted TypeScript, legacy JavaScript files might not have been upgraded yet.
You have the chance to do that now.
Check dependencies
Deprecated libraries or helpers are often maintained only for legacy code that is rarely changed and no one fully owns.
Evaluate what it uses under the hood and see if you can clean up and/or upgrade.
Verify any assumptions that the code might be making
Legacy code might be operating under assumptions that are no longer true.
I’ve seen legacy code that relies on a long-deprecated feature that still worked only because of a side effect.
An extreme case I’ve come across is a silo of self-supporting legacy code that could be removed entirely.
❗ At this point you should have well-tested code, that can no longer be considered legacy.
Make your change
Now that the code is well covered, you understand it much better, and it follows the latest guidelines and architecture, you’re good to make your change.
Comment liberally
I’d also suggest that as you work you’re very liberal with your comments. Note down any assumptions the code makes and any edge cases you may have discovered. Also, explain the change you’re making and why and include a link to your ticket.
That will help the person who comes after you. And who knows when that will be?
(Try to) Get reviews from outside the team
Look for previous contributors
Look at the git blame of the related files and ping a couple of people who worked on it most recently for reviews.
Look for affected teams
It is a good idea to ping any downstream teams for a review. They might not know how the code works, but they might know its implications.