Mocking vs Refactoring

Sometimes code refactoring removes need for advanced mocking in tests.

Recently I have published a repo with lots of examples of mocking Node system APIs in tests: node-mock-examples. One of the examples shows mocking system timers to "speed up" long test. This is coming from our production test that had a module emitting an event every minute.

1
2
3
4
5
6
7
8
const human = require("human-interval")
function startPolling() {
const interval = human("1 minute")
const poll = () => {
// emit an event
}
setInterval(poll, interval)
}

The test spied on the event and sped up the system clock to make sure the events were emitted. It looked like this

1
2
3
4
5
6
7
8
9
10
11
const sinon = require('sinon')
it('emits event after 1 minute', () => {
startPolling()
const clock = sinon.useFakeTimers()
// check event was not emitted yet
clock.tick(30000)
// check event was not emitted yet
clock.tick(30001)
// check event WAS emitted once
clock.restore()
})

But something did not work. Maybe due to spying, or multiple promises involved, Mocha test runner itself was getting confused and accelerated the test, finishing it before the assertions were made. Mocking timers is hard because they interfere with the testing framework. Similarly mocking file system can interfere with loading or saving snapshots. Maybe these brittle tests might be simpler to write and maintain if we refactored the code a little.

Refactoring

Look at the code under test in the function startPolling. It has the duration hard-coded. Why don't we allow setting the interval? We could even keep using human-friendly human-interval module. We could validate the duration of course, because we should be a little paranoid about our inputs.

1
2
3
4
5
6
7
8
9
10
const human = require("human-interval")
function startPolling(period = '1 minute') {
const interval = human(period)
console.assert(interval > 1 && interval <= human('5 minutes'),
`invalid period ${period}`)
const poll = () => {
// emit an event
}
setInterval(poll, interval)
}

Perfect, now we can run "normal" unit test that is very fast by using a short interval and avoid the fake clock song and dance.

1
2
3
4
5
6
7
8
9
10
11
12
it('emits event after period', (done) => {
const period = '5 ms'
startPolling(period)
// check event was not emitted yet
setTimeout(() => {
// check event was not emitted yet
}, 3)
setTimeout(() => {
// check event WAS emitted once
done()
}, 6)
})

The refactoring makes it simple to mold the behavior so it fits our test without any advanced techniques.

Configure then run

Often my functions are split into 2, almost like a "poor man's Curry". The first function you call just sets the arguments in the closure, and the returned function is what you actually call. The above function is not that - it starts running right away. A better example is addition that we want to observe using logs. If we use console.log we hardcode it and then we have to mock console.log; possible of course, but much harder than necessary!

1
2
3
4
function add(a, b) {
console.log('adding %j to %j', a, b)
return a + b
}

We could pass log directly as an argument to add and even use the default value of console.log

1
2
3
4
function add(a, b, log = console.log) {
log('adding %j to %j', a, b)
return a + b
}

But the signatures of public functions become longer and longer, and then we better to use a systematic way to handle optional dependencies, like using "dependency injection". Split configuration - execution on the other hand explicitly avoids this. It allows anyone to "configure" or "create" an actual "worker" function first (probably a pure function call) and then use the configured function.

If the case above it would look like this:

1
2
3
4
5
6
7
function add(log = console.log) {
return (a, b) => {
log('adding %j to %j', a, b)
return a + b
}
}
module.exports = add

Anyone using this function can simply configure it, and in most cases it would be just ()

1
2
3
4
// before - no configuration
const add = require('./add')
// after - default configurstion
const add = require('./add')()

During testing we pass a spy function

1
2
3
4
5
6
7
const sinon = require('sinon')
it('calls log', () => {
const spy = sinon.spy()
const add = require('./add')(spy)
add(2, 3)
console.assert(spy.calledOnce)
})

Function returning a function does not add much overhead, and allows quick configuration for better testability.