0

I'm trying to test a function which calls request.get() function inside it.
What I'm trying is to cover all branches of the callback function. I'm trying to achieve it without separating the callback function to a different function because it's uses variables of the upper closure.

Here's an example to illustrate.. foo.js:

var request = require('request');

function func(arg1, arg2) {
    request.get({...}, function(error, response, body) {
        // do stuff with arg1 and arg2 // want to be covered
        if (...) { // want to be covered
        ... // want to be covered
        } else if (...) { // want to be covered
        ... // want to be covered
        } else {
        ... // want to be covered
        }
    });
}

exports.func = func;

I tried to stub it with sinon as well as proxyquire.

foo.spec.js (stubbed with sinon):

var foo = require('./foo'),
var sinon = require('sinon'),
var request = require('request');

var requestStub = sinon.stub(request, 'get', function(options, callback) {
    callback(new Error('Custom error'), {statusCode: 400}, 'body');
}); // Trying to replace it with a function that calls the callback immediately so not to deal with async operations in test

foo.func(5, 3);

foo.spec.js (stubbed with proxyquire):

var requestStub = {};

var proxyquire = require('proxyquire'),
var foo = proxyquire('./foo', {'request': requestStub}),
var sinon = require('sinon');

requestStub.get = function(options, callback) {
    callback(new Error('Custom error'), {statusCode: 400}, 'body');
}; // Trying to replace it with a function that calls the callback immediately so not to deal with async operations in test

foo.func(5, 3);

Neither did work. When I tried to debug I never hit the callback function, which is suggesting that I didn't stub the request.get() method correctly making it still async operation. I would appreciate someone tell me what I did wrong in both scenarios (sinon & proxyquire) and examples on how to fix it.

Jorayen
  • 1,737
  • 2
  • 21
  • 52
  • Possibly unrelated, but your stub invokes `callbak` instead of `callback` – andyk Jun 11 '16 at 14:07
  • @andyk yea I wrote those examples by hand, I didn't copy paste them – Jorayen Jun 11 '16 at 14:28
  • I know this isn't particularly useful, but it works for me with sinon. I just copied your code sample and ran it myself. Is there anything else going on that you haven't put in the question? – MrWillihog Jun 11 '16 at 15:27
  • It looks and works as expected for me as well with Sinon. – robertklep Jun 11 '16 at 15:48
  • @robertklep I don't see something I could add to the question, I've triple checked for any things that might be useful to add but it seems that's it, it just doesn't call the callback as soon as request.get is fired, suggesting that it still asynchronous and the test ends as soon as `request.get()` is called. I'm using `mocha-sinon` tho for creating sandbox testing automatically, although I tried to disable it and it still doesn't work – Jorayen Jun 11 '16 at 16:18
  • @Jorayen what does `func` do when the callback gets called? I've tested with [this code](https://gist.github.com/robertklep/5f81851bd471007896bd69a24ba4b320) (`.yields()` is just a shortcut of what your stub is doing with the function argument). – robertklep Jun 11 '16 at 16:25
  • @robertklep I've tried to use yield, forgot to mention.. here's the actual code snippet: https://gist.github.com/Kashio/d65096458be28153016fc056fe11cf65 – Jorayen Jun 11 '16 at 16:33
  • @Jorayen so with your code, how do you determine that the stub doesn't work? I assume that you're calling `foo` with the correct arguments and that you mostly want to test if the `callback` is getting called properly for various outcomes of `request.get()`? – robertklep Jun 11 '16 at 18:58
  • @robertklep yes exactly, I call `foo()` with `attemptsLeft` bigger than 1 so It'll go to the else branch, then the `request.get()` is being called and I set a break point at the first line of the callback function that I use in it, but it never hits because the test already finish (I don't have any assertions yet in this case), so I'm assuming if the test already finished and the break point wasn't hit, than my stub which is making it from async operation to a sync one, isn't actually working. – Jorayen Jun 11 '16 at 20:48
  • @Jorayen I can imagine that setting breakpoints on the callback of a function that gets stubbed (overwritten) might confuse the debugger. – robertklep Jun 12 '16 at 07:52
  • @robertklep I set breakpoint on the stubbed function first line as-well as the stubbed function callback code (what I wish to test), neither did hit. I'm using the latest version of JetBrains Webstorm if it matters, I even tried to log something to console in my `request.get()` stub but it didn't print anything, so I guess it's not a debugger problem, – Jorayen Jun 12 '16 at 10:50
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/114467/discussion-between-jorayen-and-robertklep). – Jorayen Jun 12 '16 at 16:27
  • I've had to work on apps for the past year that can't separate "the callback function to a different function because it's uses variables of the upper closure." Do IT!!! Pull off the bandaid and refactor! If you start building on top of code like this it becomes to brittle to make any modifications to, regardless of test coverage. At the VERY least your conditional logic should be encapsulated in a function so that they can be easily unittested – dm03514 Jun 13 '16 at 01:05
  • @dm03514 I think I'll end up doing it, but I just want to know what's wrong with my stub, and why it fails, it still irritates me. – Jorayen Jun 17 '16 at 12:05

1 Answers1

0

Here is an unit test solution using sinon and mocha:

index.js:

var request = require("request");

function func(arg1, arg2) {
  request.get("https://github.com/mrdulin", function(error, response, body) {
    console.log(arg1, arg2);
    if (error) {
      console.log(error);
    } else if (response.status === 200) {
      console.log(response);
    } else {
      console.log("others");
    }
  });
}

exports.func = func;

index.spec.js:

var foo = require("./");
var sinon = require("sinon");
var request = require("request");

describe("func", () => {
  afterEach(() => {
    sinon.restore();
  });
  it("should handle error", () => {
    const logSpy = sinon.spy(console, "log");
    const getStub = sinon.stub(request, "get");
    foo.func(1, 2);
    const mError = new Error("network error");
    getStub.yield(mError, null, null);
    sinon.assert.calledWith(
      getStub,
      "https://github.com/mrdulin",
      sinon.match.func
    );
    sinon.assert.calledWith(logSpy.firstCall, 1, 2);
    sinon.assert.calledWith(logSpy.secondCall, mError);
  });

  it("should handle response", () => {
    const logSpy = sinon.spy(console, "log");
    const getStub = sinon.stub(request, "get");
    foo.func(1, 2);
    const mResponse = { status: 200 };
    getStub.yield(null, mResponse, null);
    sinon.assert.calledWith(
      getStub,
      "https://github.com/mrdulin",
      sinon.match.func
    );
    sinon.assert.calledWith(logSpy.firstCall, 1, 2);
    sinon.assert.calledWith(logSpy.secondCall, mResponse);
  });

  it("should handle other situation", () => {
    const logSpy = sinon.spy(console, "log");
    const getStub = sinon.stub(request, "get");
    foo.func(1, 2);
    const mResponse = { status: 500 };
    getStub.yield(null, mResponse, null);
    sinon.assert.calledWith(
      getStub,
      "https://github.com/mrdulin",
      sinon.match.func
    );
    sinon.assert.calledWith(logSpy.firstCall, 1, 2);
    sinon.assert.calledWith(logSpy.secondCall, "others");
  });
});

Unit test result with 100% coverage:

  func
1 2
Error: network error
    at Context.it (/Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/src/stackoverflow/37764363/index.spec.js:1:4103)
    at callFn (/Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/node_modules/mocha/lib/runnable.js:387:21)
    at Test.Runnable.run (/Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/node_modules/mocha/lib/runnable.js:379:7)
    at Runner.runTest (/Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/node_modules/mocha/lib/runner.js:535:10)
    at /Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/node_modules/mocha/lib/runner.js:653:12
    at next (/Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/node_modules/mocha/lib/runner.js:447:14)
    at /Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/node_modules/mocha/lib/runner.js:457:7
    at next (/Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/node_modules/mocha/lib/runner.js:362:14)
    at Immediate._onImmediate (/Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/node_modules/mocha/lib/runner.js:425:5)
    at runCallback (timers.js:705:18)
    at tryOnImmediate (timers.js:676:5)
    at processImmediate (timers.js:658:5)
    ✓ should handle error (48ms)
1 2
{ status: 200 }
    ✓ should handle response
1 2
others
    ✓ should handle other situation


  3 passing (58ms)

---------------|----------|----------|----------|----------|-------------------|
File           |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
---------------|----------|----------|----------|----------|-------------------|
All files      |      100 |      100 |      100 |      100 |                   |
 index.js      |      100 |      100 |      100 |      100 |                   |
 index.spec.js |      100 |      100 |      100 |      100 |                   |
---------------|----------|----------|----------|----------|-------------------|

Source code: https://github.com/mrdulin/mocha-chai-sinon-codelab/tree/master/src/stackoverflow/37764363

Lin Du
  • 88,126
  • 95
  • 281
  • 483