The most common advice about refactoring is to cover old code with tests before changing anything. This is a good suggestion — we cannot modify code if we do not have a way to validate its correctness, and tests are the best way of gaining confidence.
However, it is easier said than done. The code you need to refactor is likely to be different from the samples you saw in the unit test tutorials which usually look like this:
If the code consisted solely of single-purpose pure functions, you wouldn't need to refactor it at all. Most of the time, to-be-refactored code is hard to understand, not to mention writing tests for it. It is usually full of giant functions that do dozens of different things at the same time, implicit state manipulations, and hard-to-trace callbacks.
How to preserve a behaviour when changing a shape of code?
Let's refactor something!
Here is a piece of old code which I would like to change:
The primary purpose of this code is to render the profile page for a given user, but it does a couple of other things:
- It sends stats when somebody requests a profile and when the page is rendered
- It caches previously constructed pages for up to one hour (3600000 milliseconds) to reduce the database load
We want to preserve this behaviour, which means we need resilient tests for it.
You want to write tests which will survive code refactoring. It is extremely unpleasant if you spend time writing them, then you change code and all your tests need to be fixed.
To demonstrate the problem, I will show how we can test that stats are being sent correctly:
Check again the piece of code that sends those stats:
You can see several issues with this code:
renderProfilehas to know how to send stats — if we are to change it in the future, we will need to adjust this function
- We do not handle errors — nothing catches if a request fails
- All events are sent independently – it would be better to send stats in batches to reduce the number of requests
A right approach would be to put stats-related functionality to the separate module and then use it in the main one:
If we run tests right now, they will fail because
renderUserProfile does not send requests directly, it calls
console.log to strengthen tests during refactoring
When everything is hugely volatile, we may want to temporary sprinkle logs in important logical pieces:
On the next step, write tests asserting that correct data was logged:
Then, make sure that those logs go to the right places during refactoring:
When refactoring is done, remove all useless logs and replace them with proper assertions in tests:
This simple trick will help you keep your tests green during refactoring no matter how much code you modify. Using this method, you can test very complex logic, such as caching:
It is troublesome when you cannot rely on your tests when you need them the most. But it is much worse when your tests are green and code works incorrectly.
Use this method only as a temporary strengthening for tests during refactoring and replace all occurrences of console.log with the proper assertions as soon as you can.