4

I am trying to unit test a module by stubbing one of its dependencies, in this case the UserManager

A simplified version of the module is as follows:

// CodeHandler
module.exports = function(UserManager) {
  return {
    oAuthCallback: function(req, res) {
      var incomingCode = req.query.code;
      var clientKey = req.query.key;
      UserManager.saveCode(clientKey, incomingCode)
        .then(function(){
          res.redirect('https://test.tes');
        }).catch(function(err){
          res.redirect('back');
        }
      );
    }
  };
};

I'm stubbing the UserManager's saveCode function which returns a Promise such that it returns a resolved Promise, but when I assert that res.redirect has been called, alas at the time of the assertion res.redirect has not yet been called.

A simplified version of the unit test is:

// test
describe('CodeHandler', function() {
  var req = {
    query: {
      code: 'test-code',
      key: 'test-state'
    }
  };

  var res = {
    redirect: function() {}
  };

  var expectedUrl = 'https://test.tes';
  var ch;

  beforeEach(function() {
    sinon.stub(UserManager, 'saveCode').returns(
      new RSVP.Promise(function(resolve, reject){
        resolve();
      })
    );

    sinon.stub(res, 'redirect');

    ch = CodeHandler(UserManager);
  });

  afterEach(function() {
    UserManager.saveCode.restore();
    res.redirect.restore();
  });

  it('redirects to the expected URL', function(){
    ch.oAuthCallback(req, res);
    assert(res.redirect.calledWith(expectedUrl));
  })
});

How can I properly stub the promise such that the method under test behaves synchronously?

Dave Sag
  • 13,266
  • 14
  • 86
  • 134
  • What testing framework are you using? doesn't it have async support? – Amit Sep 29 '15 at 06:58
  • Using `mocha` and yes it has async support but that won't really help me in this case. It's not the promise that's under test but the code that reacts to the outcome of the promise. – Dave Sag Sep 29 '15 at 07:26
  • `res` is not defined in your test code. if it was a real function, you could call `done` inside of it (that you get as an input to your test function) – Amit Sep 29 '15 at 08:09
  • sorry yes you are right, I forgot to define `res` in this extract from my actual test. I'll fix that for completeness. – Dave Sag Sep 29 '15 at 22:09

2 Answers2

2

I've worked out a solution using sinon-stub-promise.

describe('CodeHandler', function() {
  var req = {
    query: {
      code: 'test-code',
      key: 'test-state'
    }
  };
  var ch;
  var promise;

  var res = {
    redirect: function() {}
  };

  beforeEach(function() {
    promise = sinon.stub(UserManager, 'saveCode').returnsPromise();
    ch = CodeHandler(UserManager);
    sinon.stub(res, 'redirect');
  });

  afterEach(function() {
    UserManager.saveCode.restore();
    res.redirect.restore();
  });

  describe('can save code', function() {
    var expectedUrl = 'https://test.tes';

    beforeEach(function() {
        promise.resolves();
    });

    it('redirects to the expected URL', function(){
      ch.oAuthCallback(req, res);
      assert(res.redirect.calledWith(expectedUrl));
    });
  });

  describe('can not save code', function() {
    var expectedUrl = 'back';

    beforeEach(function() {
        promise.rejects();
    });

    it('redirects to the expected URL', function(){
      ch.oAuthCallback(req, res);
      assert(res.redirect.calledWith(expectedUrl));
    })
  })
});

This works perfectly.

Dave Sag
  • 13,266
  • 14
  • 86
  • 134
1

Well, the easiest thing would be not to stub it to run synchronously at all since that might change execution order and use Mocha's built in promises support (or jasmine-as-promised if using jasmine).

The reason is there can be cases like:

somePromise.then(function(){
    doB();
});
doA();

If you cause promises to resolve synchronously the execution order - and thus output of the program changes, making the test worthless.

On the contrary, you can use the test syntax:

describe("the test", () => { // use arrow functions, node has them and they're short
    it("does something", () => {
        return methodThatReturnsPromise().then(x => {
           // assert things about x, throws will be rejections here
           // which will cause a test failure, so can use `assert`
        });
    });
});

You can use the even lighter arrow syntax for single lines which makes the test even less verbose:

describe("the test", () => { // use arrow functions, node has them and they're short
  it("does something", () =>
      methodThatReturnsPromise().then(x => {
         // assert things about x, throws will be rejections here
         // which will cause a test failure, so can use `assert`
      });
  );
});

In RSVP, you can't set the scheduler as far as I know so it's quite impossible to test things synchronously anyway, other libraries like bluebird let you do it at your own risk, but even in libraries that let you do it it's probably not the best idea.

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • 1
    It's not the promise itself that's under test but the code which is responding to the promise that is under test. – Dave Sag Sep 29 '15 at 07:29
  • @DaveSag then that code should also return a promise (put `return` before UserManager.saveCode`) so you can test it and then use the async syntax. – Benjamin Gruenbaum Sep 29 '15 at 07:33