Kevin Kevin - 2 months ago 54
Javascript Question

sinon stub with es6-promisified object

Ok my setup is as follows:
Using node 6.2, es6-promisify, sinon, sinon-as-promised, and babel to transpile support for es6 import/export.

My code under test looks something like this:

const client = restify.createJsonClient({
url: 'http://www.example.com'
});
export let get = promisify(client.get, {thisArg: client, multiArgs: true});

export default function* () {
yield get('/some/path');
}


And then in my test file I have something like this:

import * as m from mymodule;
it('should fail', function(done) {
let stub = sinon.stub(m, 'get').rejects('i failed');
client.get('/endpoint/that/leads/to/mymodule/call', function(err, req, res, data) {
stub.called.should.be.eql(true); // assertion fails!!
done();
}
});


I've also tried stubbing the original client.get call, and that doesn't work either. The only thing I've gotten to work is promisifying on the fly with each call, and stubbing the original client.get, which seems pretty lame. E.g.:

export const client = restify.createJsonClient({
url: 'http://www.example.com'
});
function get() {
return promisify(client.get, {thisArg: client, multiArgs: true});
}

export default function* () {
yield get('/some/path');
}


And test code doing this:

import {module_client} from mymodule;
it('should fail', function(done) {
let stub = sinon.stub(module_client, 'get').yields('i failed');
client.get('/endpoint/that/leads/to/mymodule/call', function(err, req, res, data) {
stub.called.should.be.eql(true); // assertion succeeds
done();
}
});


And so the question, if it's not completely obvious, is why does my original code not work? And is there a way to make the stub work without promisifying the original restify each time (e.g. how do other people get this sort of thing working)?

EDIT:

Current code looks like this:

const client = restify.createJsonClient({
url: 'http://www.example.com'
});

export let get = promisify(client.get, {thisArg: client, multiArgs: true});

export default function*() {
try {
console.log(exports.get); // <= a large sinon stub object, I'll post that below
yield exports.get(); // <= throws here, "exports.get is not a function"
}
catch(ex) {
log.error('got an error', ex);
throw ex;
}
}


The console.log prints the following:

{ [Function: proxy]
isSinonProxy: true,
reset: [Function],
invoke: [Function: invoke],
named: [Function: named],
getCall: [Function: getCall],
getCalls: [Function],
calledBefore: [Function: calledBefore],
calledAfter: [Function: calledAfter],
withArgs: [Function],
matches: [Function],
printf: [Function],
calledOn: [Function],
alwaysCalledOn: [Function],
calledWith: [Function],
calledWithMatch: [Function],
alwaysCalledWith: [Function],
....


EDIT2:

And FWIW, the babel generated code is producing this:

let get = exports.get = (0, _es6Promisify2.default)(client.get, { thisArg: client, multiArgs: true });


EDIT3:

Ok super weird. I changed my source to do this instead:

const client = restify.createJsonClient({
url: 'http://www.example.com'
});

export let get = promisify(client.get, {thisArg: client, multiArgs: true});

export default function*() {
try {
let thePromise = exports.get(); // e.g. call exports.get on separate line from the yield
yield thePromise; // and the throw now says 'undefined is not a function'. I should note that in both cases, the stack trace shows the error on node_modules/co/index.js at line 65.
}
catch(ex) {
log.error('got an error', ex);
throw ex;
}
}

Answer

The problem ultimately has to do with how ES6 import/exports work, and specifically, how they make your code look better but prevent easy spying/stubbing.

Take this example module:

// my-module.js
function someFunction() {
  console.log('original');
};

export let get = someFunction;

export default function() {
  get();
};

A test case for that code could look like this:

import * as sinon from 'sinon';
import * as should from 'should';
import setup, * as myModule from './my-module';

it('should call get()', () => {
  let stub = sinon.stub(myModule, 'get');
  setup();
  stub.called.should.eql(true);
});

You'll see that the original get() gets called, and not the stub. This is because in the module, get is a local (to the module) reference. Sinon is stubbing another reference to the same function, in the exported object.

To make this work, instead of using a local reference in the module, you need to use the one in the exported object:

export default function() {
  exports.get();
};

Which, alas, makes for uglier code.

Comments