How to use console.log to simplify refactoring

How a simple trick with console.log can help you with refactoring

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:

A typical unit-test tutorial

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:

Render user profile

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.

Resilient tests?

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:

Asserting fetch in tests

Check again the piece of code that sends those stats:

You can see several issues with this code:

  1. renderProfile has to know how to send stats — if we are to change it in the future, we will need to adjust this function
  2. We do not handle errors — nothing catches if a request fails
  3. 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:

Separate module for stats

If we run tests right now, they will fail because renderUserProfile does not send requests directly, it calls stats.trackUserRequest instead.

Using console.log to strengthen tests during refactoring

When everything is hugely volatile, we may want to temporary sprinkle logs in important logical pieces:

console.log next to sending stats

On the next step, write tests asserting that correct data was logged:

Checking if the correct string was logged

Then, make sure that those logs go to the right places during refactoring:

Logs stick with the relevant code

When refactoring is done, remove all useless logs and replace them with proper assertions in tests:

Logs did the job and can be removed

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:

Tests for caching logic

Final words

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.

Happy refactoring!