When you write your module (let's say a Node JavaScript one), you often expose public API, but internally your code accesses the outside world via own code. Testing the public API is the best, but often it is hard, since your code interacts with the file system, user or network.
How do you test the exposed API when you need to mock the rest of the system? I think it is preferable to NOT mock your own code modules, but to mock the interface to the external world.
In principle, if your public API calls two internal functions fn1
and fn2
, which in turn call Node system libraries (like fs
or http
),
then you probably should stub the Node system libraries and NOT
the internal functions fn1
and fn2
.
1 | public API <-----------------------+ Testing public API |
hint the above diagram was created using AsciiDraw
Example
My examples comes from my own library wrapping the Git tool
ggit. The library has many public functions
in its public API, methods like blame
and commits
. A typical use
case is someone writing a Node program showing history for each line of a
tracked file
1 | const blame = require('ggit').blame |
How can we test this public function from ggit
? Well, src/blame.js
makes
use of two calls internally - one is to internal module ./exec.js
which
routes to Node's built-in child_process.exec
, and another directly
to Node's built-in fs.existsSync
function. Do we somehow overwrite
calls to ./exec.js
with stubs controlled from the tests? That is difficult,
and requires exposing ./exec.js
module and being able to overwrite it
on the fly. One can use helpers that clear Node module cache and then force
loading ./exec.js
again, but substituting the stubbed version. One
could use proxyquire or really-need to do this,
and the test would look like this
1 | const reallyNeed = require('really-need') |
Painful, isn't it? This mocking targets immediate dependencies of the
module under test, but their are internal modules that should be replaceable
easily. Yet now, the test makes it hard to test ./exec.js
or replacing
it with a different, better module, like execa. It is hard, because
changing it requires reworking lots and lots of complicated tests
that mostly setup mocks of our unstable code!
The person doing the refactoring has to update tests and make sure the
Node module cache is used correctly, and the modules under test are
loaded in the right order. I often feel like pulling so much dead weight
when refactoring tests, just like this tractor.
Mocking system libraries
Instead of mocking (or stubbing) the immediate internal modules around the
module under test, let us mock the system APIs our code is expected to
use. For example, instead of mocking ./exec.js
function, we can mock
the child_process.exec
that will be called at some point in order
for our module to work.
Mocking a simple method
First, let me show the simple stubbing of system dependency - overwriting
fs.existsSync
call. We can use the excellent Sinon library to
do this quickly.
1 | const sinon = require('sinon') |
All we have do to make sure the call fs.existsSync('foo.txt')
returns true
for fake file foo.txt
is to stub it.
1 | sinon.stub(fs, 'existsSync').withArgs(filename).returns(true) |
Of course after we are done with a test we should restore the original
fs.existsSync
method. Or we can only stub the first call - but we should
still cleanup the stub after the test
1 | sinon.stub(fs, 'existsSync') |
Similarly, we can stub a property or promise-returning method
1 | sinon.stub(object, "propertyName", newValue) // mock property value |
For full documentation see sinon stubs
Mocking event emitter
Mocking Node's child_process.spawn
method is slightly more complex, because
it returns a ChildProcess
event emitter. But I have written a utility
that does exactly that - stubs spawn
or exec
system functions.
I even made stub-spawn-once testing-friendly because it
automatically cleans up each stub after using it once.
For example, the ./blame.js
calls ./exec.js
with the following command
(assuming "foo.txt" filename and line number 10)
1 | git blame --porcelain -L 10,10 foo.txt |
Then git blame
command produces something that looks like this
1 | 6f272d6aef8a1d19e559792d9493d07d4218ea09 10 10 1 |
Our test then can mock both fs.existsSync
and child_process.exec
Node
built-in API functions like this
1 | const sinon = require('sinon') |
Note that we no longer care or depend on our internal modules. Instead we
only care about interacting with the operating system. This is much easier
to maintain. Think about a developer updating a test for ./blame.js
,
he or she can see the expected Git command, and its mock output, and can even
run the command in the terminal!
And if we change the internal ./exec.js
code or even replace it
with another library for running shell scripts, our tests still going to
execute the same operating system calls, and should be able to parse the same
command output. Truly the ./blame.js
test is now testing only a single
source file and not several.
Examples
Take a look at these spec files showing mocking stable Node API calls, rather than internal modules.
-
The above example with snap- and schema-shot testing (see the "Bonus" section below)
-
Stubs file load like
1
2
3
4
5sinon.stub(fs, 'existsSync').withArgs(fullPath).returns(true)
sinon
.stub(fs, 'readFileSync')
.withArgs(fullPath, 'utf8')
.returns(configString) -
Shows how easy it is to stub HTTP(s) requests using nock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const nock = require('nock')
const description = 'cool project, bro'
beforeEach(() => {
nock('https://api.github.com')
.get('/repos/no-such-user/does-not-exist')
.reply(200, { description })
})
it('can mock non-existent repo', () => {
const url = '[email protected]:no-such-user/does-not-exist.git'
return repoDescription(url).then(text => {
la(description === text, 'wrong description returned', text)
})
})Every
nock
intercept is automatically removed after single use, so nothing needs to be cleaned up.
and lots more examples in their own repo node-mock-examples.
hint if you do not know what HTTP requests are actually happening, run
the tests with DEBUG=nock.* ...
or just DEBUG=nock.interceptor ...
environment variables to see Nock's messages.
Final thoughts
- Mocking your own internal modules is hard: they are cached by the Node module system, unstable, often undocumented.
- Mocking OS calls is easier; API is stable and well documented
- Libs like
sinon
andstub-spawn-once
make stubbing API methods a breeze
Bonus
I love using snapshot testing to quickly assert everything
about received result. Just wrap the promise returned from the call
blame(filename, line)
1 | const snapshot = require('snap-shot') |
Similarly, we can skip mocking even OS results by using schema snapshots in this case. We are interested in the schema of the result, rather than exact values.
1 | const schemaShot = require('schema-shot') |
Happy testing!