Home > Software design >  Sinon - how to spy on the save() method in a mongoose model
Sinon - how to spy on the save() method in a mongoose model

Time:09-20

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?

CodePudding user response:

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"
  • Related