[SOLVED] Sinon – how to spy on the save() method in a mongoose model

Issue

This Content is from Stack Overflow. Question asked by lhoogenraad

I’m writing unit tests for an API that uses MongoDB, with mongoose being used to access the database.

I have a model file which defines and exports a mongoose model:

const { Schema, model } = require('mongoose');

const TravelSchema = new Schema({
    staffFirstName: {
        type: String,
        required: true,
    },
    staffLastName: {
        type: String,
        required: false,
    },
    kmTravelled: {
        type: Number,
        required: true,
    },
});

module.exports = model('travel', TravelSchema);

I then have a method that takes a req and res, constructs an object out of the attributes of the req.body, then saves it to the database:

const TravelSchema = require('../models/travel_usage');

exports.submitTravelUsage = async function (req, res) {
    try {
        var data = {
            staffFirstName: req.body.staffFirstName,
            staffLastName: req.body.staffLastName,
            kmTravelled: req.body.kmTravelled,
        }

        var travelUsage = new TravelUsage(data);

        travelUsage.save();

        return res.status(201).json(data);

        }catch(error){
            console.log(error.message);
            return res.status(400).json({message: error.message});
        }

My test file for the backend method looks something like:

const request = require('supertest');
const app = require('../src/app');
const sinon = require('sinon');
const assert = require('assert').strict;
const TravelSchema = require('../models/travel_usage');

describe('POST /travelUsage - returns 201', function () {
    it('responds with 201 created - basic POST body', function (done) {
        this.timeout(7500);

        var save = sinon.spy(TravelSchema, 'save');

        request(app)
            .post('/travelUsage')
            .set('Content-Type', 'application/json')
            .send(travelUsageData)
            .then(function (response) {
                assert.equal(response.status, 201);
                sinon.assert.calledOnce(save);
                done();
            })
            .catch(err => done(err));
    })
})

But when the test is run an error is thrown saying TypeError: Cannot stub non-existent property save. It seems that the model object exported by the model file doesn’t contain the save() method, but when a new model is created with data it gains the “save()` method, so is there a better way to spy on the save method being called in the submitTravelUsage method?



Solution

There is no .save() method on mongoose schema. A model instance and A document have the .save() method. Please read the Models doc.

So you should stub the .save() method on the mongoose document(a model instance).

And your code is a commonJS version, so we will use proxyquire to help us stub the default exports(module.exports = model('travel', TravelSchema);)

Besides, we don’t need to use the assert built-in module of Node.js. Sinon.js already provides a set of assertions

E.g.

./models/travel_usage.js:

const { Schema, model } = require('mongoose');

const TravelSchema = new Schema({
  staffFirstName: {
    type: String,
    required: true,
  },
  staffLastName: {
    type: String,
    required: false,
  },
  kmTravelled: {
    type: Number,
    required: true,
  },
});

module.exports = model('travel', TravelSchema);

app.js:

const express = require('express');
const TravelUsage = require('./models/travel_usage');

const app = express();

app.use(express.json());
app.post('/travelUsage', function (req, res) {
  try {
    var data = {
      staffFirstName: req.body.staffFirstName,
      staffLastName: req.body.staffLastName,
      kmTravelled: req.body.kmTravelled,
    };
    var travelUsage = new TravelUsage(data);
    travelUsage.save();
    return res.status(201).json(data);
  } catch (error) {
    console.log(error.message);
    return res.status(400).json({ message: error.message });
  }
});

module.exports = app;

app.test.js:

const request = require('supertest');
const sinon = require('sinon');
const proxyquire = require('proxyquire');

describe('POST /travelUsage - returns 201', function () {
  it('responds with 201 created - basic POST body', function (done) {
    const travelUsageDocumentStub = {
      save: sinon.stub(),
    };
    const TravelUsageModelStub = sinon.stub().returns(travelUsageDocumentStub);
    const app = proxyquire('./app', {
      './models/travel_usage': TravelUsageModelStub,
    });
    const travelUsageData = {
      staffFirstName: 'a',
      staffLastName: 'b',
      kmTravelled: 'test',
    };

    request(app)
      .post('/travelUsage')
      .set('Content-Type', 'application/json')
      .send(travelUsageData)
      .then(function (response) {
        sinon.assert.match(response.status, 201);
        sinon.assert.match(response.body, travelUsageData);
        sinon.assert.calledWithExactly(TravelUsageModelStub, travelUsageData);
        sinon.assert.calledOnce(travelUsageDocumentStub.save);
        done();
      });
  });
});

Test result:

  POST /travelUsage - returns 201
    ✓ responds with 201 created - basic POST body (2302ms)


  1 passing (2s)

------------------|----------|----------|----------|----------|-------------------|
File              |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
------------------|----------|----------|----------|----------|-------------------|
All files         |    93.94 |      100 |      100 |    93.55 |                   |
 73767036         |    92.86 |      100 |      100 |    92.86 |                   |
  app.js          |    84.62 |      100 |      100 |    84.62 |             18,19 |
  app.test.js     |      100 |      100 |      100 |      100 |                   |
 73767036/models  |      100 |      100 |      100 |      100 |                   |
  travel_usage.js |      100 |      100 |      100 |      100 |                   |
------------------|----------|----------|----------|----------|-------------------|

package versions:

"express": "^4.17.1",
"mongoose": "^5.11.9",
"sinon": "^7.5.0",
"proxyquire": "^2.1.3",
"supertest": "^4.0.2"


This Question was asked in StackOverflow by lhoogenraad and Answered by slideshowp2 It is licensed under the terms of CC BY-SA 2.5. - CC BY-SA 3.0. - CC BY-SA 4.0.

people found this article helpful. What about you?