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 | const human = require("human-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 | const sinon = require('sinon') |
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 | const human = require("human-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 | it('emits event after period', (done) => { |
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 | function add(a, b) { |
We could pass log
directly as an argument to add
and even use the
default value of console.log
1 | function add(a, b, log = console.log) { |
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 | function add(log = console.log) { |
Anyone using this function can simply configure it, and in most cases it would
be just ()
1 | // before - no configuration |
During testing we pass a spy function
1 | const sinon = require('sinon') |
Function returning a function does not add much overhead, and allows quick configuration for better testability.