Peter Martinson
Peter Martinson

Reputation: 97

How to properly test functions that return Mongoose queries as Promises

I'm trying to write a basic unit test to work on the function below, but can't get it to work. How do I test that something like a proper npm-express response is returned?

I already looked at Using Sinon to stub chained Mongoose calls, https://codeutopia.net/blog/2016/06/10/mongoose-models-and-unit-tests-the-definitive-guide/, and Unit Test with Mongoose, but still can't figure it out. My current best guess, and the resulting error, is below the function to be tested. If possible, I don't want to use anything but Mocha, Sinon, and Chai.expect (i.e. not sinon-mongoose, chai-as-expected, etc.). Any other advice, like what else I can/should test here, is welcome. Thank you!

The function to be tested:

function testGetOneProfile(user_id, res) {
  Profiles
    .findOne(user_id)
    .exec()
    .then( (profile) =>  {
      let name   = profile.user_name,
        skills = profile.skills.join('\n'),
        data   = { 'name': name, 'skills': skills };
      return res
        .status(200)
        .send(data);
    })
    .catch( (err) => console.log('Error:', err));
}

My current best-guess unit test:

const mongoose = require('mongoose'),
      sinon    = require('sinon'),
      chai     = require('chai'),
      expect   = chai.expect,
      Profile  = require('../models/profileModel'),
      foo      = require('../bin/foo');

mongoose.Promise = global.Promise;

describe('testGetOneProfile', function() {
  beforeEach( function() {
    sinon.stub(Profile, 'findOne');
  });
  afterEach( function() {
    Profile.findOne.restore();
  });

  it('should send a response', function() {
    let mock_user_id = 'U5YEHNYBS';
    let expectedModel = {
      user_id: 'U5YEHNYBS',
      user_name: 'gus',
      skills: [ 'JavaScript', 'Node.js', 'Java', 'Fitness', 'Riding', 'backend']
    };
    let expectedResponse = {
      'name': 'gus',
      'skills': 'JavaScript, Node.js, Java, Fitness, Riding, backend'
    };
    let res = {
      send: sinon.stub(),
      status: sinon.stub()
    };
    sinon.stub(mongoose.Query.prototype, 'exec').yields(null, expectedResponse);
    Profile.findOne.returns(expectedModel);

    foo.testGetOneProfile(mock_user_id, res);

    sinon.assert.calledWith(res.send, expectedResponse);
  });
});

The test message:

  1) testGetOneProfile should send a response:
     TypeError: Profiles.findOne(...).exec is not a function
      at Object.testGetOneProfile (bin\foo.js:187:10)
      at Context.<anonymous> (test\foo.test.js:99:12)

Upvotes: 2

Views: 1817

Answers (1)

Jani Hartikainen
Jani Hartikainen

Reputation: 43273

This is a bit of a tricky scenario. The problem here is that the findOne stub in your test returns the model object - instead, it needs to return an object which contains a property exec which in turn is a promise-returning function that finally resolves into the model value... yeah, as mentioned, it's a bit tricky :)

Something like this:

const findOneResult = {
  exec: sinon.stub().resolves(expectedModel)
}

Profile.findOne.returns(findOneResult);

You also need to have the status function on the response object return an object containing a send function

//if we set up the stub to return the res object
//it returns the necessary func
res.status.returns(res);

I think you shouldn't need to change anything else in the test and it might work like that. Note that you sinon 2.0 or newer for the resolves function to exist on the stub (or you can use sinon-as-promised with sinon 1.x)

This post goes into a bit more detail on how you can deal with complex objects like that: https://codeutopia.net/blog/2016/05/23/sinon-js-quick-tip-how-to-stubmock-complex-objects-such-as-dom-objects/

Upvotes: 3

Related Questions