node: mocking a function with a callback argument

Viewed 4483

I am trying to write unit tests for a function that reads a jsonfile into an object. I read the file with

jsonfile.readFile(filename, function (err, obj) {
  //...
});

For my unit tests, I want to mock this function so that, rather than actually reading the file, it will simply return a fixed json block and pass it into the callback.

What I'm having trouble with is how to mock the function. I've seen sinon, which says it supports mocking functions, but I can't find anything that describes how to actually define custom behavior for the function I'm mocking. Sinon looks like it allows me to define what I want the function to return, how often I expect it to be called, etc, but not actually define a mocked function.

Basically, I want something like this:

mock(jsonfile, 'readFile', function(filename, callback) {
    callback(null, {attr1:"foo"});
});

How do I do this with sinon?

3 Answers

But actually, why don't you just replace readFile by a function with the same definition (so that it doesn't break the code using it). And just return your mock data.

jsonfile.readFile = function(filePath, callback) {
    callback(null, { mockData: "foo" });
};

easy as that.

Otherwise, you can use a Proxy if you don't want to deal with the definition :

const jsonfile = {
    readFile: function(filename, callback) {
        callback();
    }
};

// intercept every call to readFile and always return the mock data
jsonfile.readFile = new Proxy(jsonfile.readFile, {
    apply: function(target, thisArg, args) {
        return args[1](null, { someMocking: "" });
    }
});

// call readFile as usual
jsonfile.readFile('testfile', function(err, result) {
    console.log(result);
});

Proxies work as interceptors.

https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Proxy

This is not straightforward in testing because it involved callbacks. You need to test wether a callback you passed to readFile was called with right arguments, which in this case is the dummyFile.

    import sinon from 'sinon'
    import jsonfile from './jsonfile'

    const jsonFileMock = sinon.spy(jsonfile, 'readFile');
    const callback = sinon.spy();

    jsonfile.readFile(callback);
    jsonFileMock.callsArgWith(1, 'dummyFileName');

    expect(callback.calledWith('dummyFileName')).to.be.true;
    jsonFileMock.restore();

If you want to abstract this into a function, than it can be something like :

function mock(module, method, ...callbacks){
  const stub = sinon.stub(jsonfile, 'readFile');
  callbacks.forEach((callback, index) => {
    stub.callsArgWith(index, callback);
  });
}

The function I was looking for is stub.callsFake():

> Thing = {
...  meth : function() { console.log(1) }
... }
> Thing.meth()
1
> var stub = sinon.stub(Thing, 'meth')
> stub.callsFake(function() { console.log(2) })
> Thing.meth()
2
> stub.restore()
> Thing.meth()
1

It doesn't look like mock is capable of what I want to do.

Related