Search code examples
node.jsmongoosepromisesinonstub

How to stub Mongoose model methods in unit tests with sinon


I have a method getTask() that looks like this:

const Task = require('./model');
const User = require('../users/model')

module.exports = async function getTask (key) {
    const task = await Task.findOne({key}).exec()
    const user = await User.findById(task.author).exec()
    task.author = user
    return task
};

Basically, I just find my task by key, find the author in users collection, combine them together and then in my endpoint I call it like this:

app.get('/task/:key', async (req, res, next) => {
    const task = await getTask(req.params.key)
    res.status(200).json(task)
})

This is how I'm trying to test this method:

const { expect } = require('chai')
const sinon = require('sinon')
const sinonTest = require('sinon-test')

const Task = require('./model');
const User = require('../users/model')
const getTask = require('./services')

const test = sinonTest(sinon);

describe('Task CRUD operations', () => {

    it('should find task and user and return together', test(async function() {
        const mockUser = new User({ id: '5e684ebacb19f70020661f44', username: 'testuser' });
        const mockTask = new Task({ id: '5e684ececb19f70020661f45', key: 'TEST-1', author: mockUser });

        const mockTaskFindOne = {
            exec: function () {
                return mockTask
            }
        };

        const mockUserFindById = {
            exec: function () {
                return mockUser
            }
        };

        this.stub(Task, 'findOne').returns(mockTaskFindOne)
        this.stub(User, 'findById').returns(mockUserFindById)
        const task = getTask('TEST-1')

        sinon.assert.calledWith(Task.findOne, { key: 'TEST-1' })
        sinon.assert.calledWith(User.findById, '5e684ebacb19f70020661f44')
        // TODO: assert returned task is what I expect
    }));
});

So I want to assert that Task.findOne() and User.findById() are called with correct params and in the end verify the returned task data.

Right now the only last line is not working where I assert User.findById was called:

  Task CRUD operations
spec.js:54
    1) should find task and user and return together
spec.js:88
  0 passing (17ms)
base.js:319
  1 failing
base.js:332
  1) Task CRUD operations
       should find task and user and return together:
     AssertError: expected findById to be called with arguments 
      at Object.fail (node_modules/sinon/lib/sinon/assert.js:107:21)
      at failAssertion (node_modules/sinon/lib/sinon/assert.js:66:16)
      at Object.assert.<computed> [as calledWith] (node_modules/sinon/lib/sinon/assert.js:92:13)
      at Context.<anonymous> (tasks/services.test.js:36:22)
      at callSandboxedFn (node_modules/sinon-test/lib/test.js:103:25)
      at Context.sinonSandboxedTest (node_modules/sinon-test/lib/test.js:131:26)
      at processImmediate (internal/timers.js:445:21)

I know it has something to do with Promises and such but I'm a complete newbie.

What am I doing wrong?

My Task model:

const mongoose = require('mongoose');

let TaskSchema = new mongoose.Schema({
    key: String,
    author: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User'
    }
});

module.exports = mongoose.model('Task', TaskSchema);

My User model:

const mongoose = require('mongoose');
const passportLocalMongoose = require('passport-local-mongoose');

let UserSchema = new mongoose.Schema({
    username: String,
    password: String
});

UserSchema.plugin(passportLocalMongoose);

module.exports = mongoose.model('User', UserSchema);

Solution

  • Based on getTask() function, User.findById() is called with task.author. This means sinon.assert.calledWith(User.findById, mockUser). Thats it.

    I created this simple example without sinon-test to make sure that the change run correctly.

    // @file stackoverflow.spec.js
    const { expect } = require('chai');
    const sinon = require('sinon');
    
    const Task = require('./task.model');
    const User = require('./user.model');
    const getTask = require('./services');
    
    describe('Task CRUD operations', function () {
      const sandbox = sinon.createSandbox();
      it('should find task and user and return together', async function () {
        const mockUser = { id: '5e684ebacb19f70020661f44', username: 'testuser' };
        const mockTask = { id: '5e684ececb19f70020661f45', key: 'TEST-1', author: mockUser };
    
        const mockTaskFindOne = sandbox.stub(Task, 'findOne');
        mockTaskFindOne.returns({
          exec: () => mockTask,
        });
    
        const mockUserFindById = sandbox.stub(User, 'findById');
        mockUserFindById.returns({
          exec: () => mockUser,
        });
    
        const task = await getTask('TEST-1');
        // Verify task.
        expect(task).to.deep.equal(mockTask);
        // Verify stub.
        expect(mockTaskFindOne.calledOnce).to.equal(true);
        expect(mockTaskFindOne.args[0][0]).to.deep.equal({ key: 'TEST-1' });
        // This is the same with above.
        expect(mockTaskFindOne.calledWith({ key: 'TEST-1' })).to.equal(true);
        expect(mockUserFindById.calledOnce).to.equal(true);
        expect(mockUserFindById.args[0][0]).to.deep.equal(mockUser);
        // This is the same with above.
        expect(mockUserFindById.calledWith(mockUser)).to.equal(true);
    
        // Restore stub.
        mockTaskFindOne.restore();
        mockUserFindById.restore();
        // Restore sandbox.
        sandbox.restore();
      });
    });
    

    Pass when I run it using mocha.

    $ mocha stackoverflow.spec.js 
    
    
      Task CRUD operations
        ✓ should find task and user and return together
    
    
      1 passing (11ms)
    
    $
    

    Hope this helps.