0
votes

I am trying to unit test a Node.js+Express REST API. It's all very standard CRUD stuff and the backend database library is MongoDB being called with Mongoose. So far, so standard...

I want to use SuperTest to test my API controllers/routes, so naturally I need to stub out the database calls

This is where I'm struggling, especially with chained calls such as .limit() and .skip() on my query results.

Trying to stub the mongoose DocumentQuery limit function with sinon doesn't work, e.g.

const stubQuery = sinon.stub(mongoose.DocumentQuery, 'limit')
.callsFake(()=>{})

Results in an error

Trying to stub property 'limit' of undefined

I've tried using the sinon-mongoose library but I can't get that to work in the context of my API calls in SuperTest. It seems to only be for running tests on the models directly which I'm not doing

My test looks like this

const app = require('../server').app;

// stub out model.find()
const stubModelFind = sinon.stub(mongoose.Model, 'find').callsFake(() => {
  return [ { foo: "bar" /* fake documents */ } ]
})

describe('Thing API', () => {
  it('returns some things', (done) => {
    request(app)
      .get('/api/things')
      .expect(function(res) {
        expect(res.body).to.be.an.an('array')
        // Rest of my tests / validation checks here
      })
      .expect(200, done);
  });
})

This test fails with this.model.find(...).limit is not a function as my service (which is the layer in my code that talks to the DB via Mongoose) looks a little like this (where this.model is an instance of a Mongoose model)

let items = await this.model.find(query)
  .limit(limit)
  .skip(skip);
1

1 Answers

1
votes

Here is the integration test solution, in order to stub chain methods, you need use stub.returnsThis();

E.g.

server.ts:

import express from 'express';
import categories from './model';
import http from 'http';

const app = express();
app.get('/api/things', async (req, res) => {
  const items = await categories
    .find({ name: 'book' })
    .limit(10)
    .skip(0);
  res.json(items);
});

const server = http.createServer(app).listen(3000, () => {
  console.info('Http server is listening on http://localhost:3000');
});

export { server };

model.ts:

import { model, Schema } from 'mongoose';

const modelName = 'category';

const categorySchema = new Schema(
  {
    name: String,
    parent: { type: Schema.Types.ObjectId, ref: modelName },
    slug: { type: String },
    ancestors: [{ _id: { type: Schema.Types.ObjectId, ref: modelName }, name: String, slug: String }],
  },
  {
    collection: 'category-hierarchy_categories',
  },
);

const categories = model(modelName, categorySchema);

export default categories;

server.integration.spec.ts:

import request from 'supertest';
import { server } from './server';
import { expect } from 'chai';
import sinon from 'sinon';
import categories from './model';

after((done) => {
  server.close(done);
});

describe('Thing API', () => {
  it('returns some things', (done) => {
    const limitStub = sinon.stub().returnsThis();
    const skipStub = sinon.stub().returns([{ foo: 'bar' }]);
    sinon.stub(categories, 'find').callsFake((): any => {
      return {
        limit: limitStub,
        skip: skipStub,
      };
    });

    request(server)
      .get('/api/things')
      .expect((res) => {
        expect(res.body).to.be.an('array');
        expect((categories.find as sinon.SinonStub).calledWith({ name: 'book' })).to.be.true;
        expect(limitStub.calledWith(10)).to.be.true;
        expect(skipStub.calledWith(0)).to.be.true;
      })
      .expect(200, done);
  });
});

Integration test result with 100% coverage:

Http server is listening on http://localhost:3000
  Thing API
    ✓ returns some things


  1 passing (43ms)

----------------------------|----------|----------|----------|----------|-------------------|
File                        |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------------------------|----------|----------|----------|----------|-------------------|
All files                   |      100 |      100 |      100 |      100 |                   |
 model.ts                   |      100 |      100 |      100 |      100 |                   |
 server.integration.spec.ts |      100 |      100 |      100 |      100 |                   |
 server.ts                  |      100 |      100 |      100 |      100 |                   |
----------------------------|----------|----------|----------|----------|-------------------|

Source code: https://github.com/mrdulin/mongoose5.x-lab/tree/master/src/stackoverflow/58787092