5

I have read the following posts on Stack Overflow:

Unit Test with Mongoose

Mocking/stubbing Mongoose model save method

I have also looked into mockgoose but I would prefer to use testdouble or sinon to stub/mock my database calls.

The information found here is probably what comes closest to what I would like to do. But I can't quite wrap my head around it. The difference, I think, is that I'm trying to test a route in my api and not the Mongoose model directly. Here is my code:

server.ts

import * as express from 'express';
const app = express()
import { createServer } from 'http';
const server = createServer(app);
import * as ioModule from 'socket.io';
const io = ioModule(server);


import * as path from 'path';
import * as bodyParser from 'body-parser';
import * as helmet from 'helmet';
import * as compression from 'compression';
import * as morgan from 'morgan';

// Database connection
import './server/db';

// Get our API routes and socket handler
import { api } from './server/routes/api'
import { socketHandler } from './server/socket/socket';

// Helmet security middleware
app.use(helmet());

// Gzip compression middleware
app.use(compression());

// Morgan logging middleware
app.use(morgan('common'));

// Parsers for POST data
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

// Point static path to dist
app.use(express.static(path.join(__dirname, 'dist')));

// Set our api routes
app.use('/api', api);

// Catch all other routes and return the index file
app.get('*', (req: any, res: any) => {
    res.sendFile(path.join(__dirname, 'dist/index.html'));
});

/**
 * Get port from environment and store in Express.
 */
const port = process.env.PORT || '3000';
app.set('port', port);


/**
 * Listen on provided port, on all network interfaces.
 */
server.listen(port, () => console.log(`API running on localhost:${port}`));

io.on('connection', socketHandler);

export { server };

/server/db.ts

import * as mongoose from 'mongoose';
// Enter database URL and delete this comment
const devDbUrl = 'mongodb://localhost:27017/book-trade';
const prodDbUrl = process.env.MONGOLAB_URI;

const dbUrl = devDbUrl || prodDbUrl;

mongoose.connect(dbUrl);

(<any>mongoose).Promise = global.Promise;

mongoose.connection.on('connected', () => {
    console.log('Mongoose connected to ' + dbUrl);
});

mongoose.connection.on('disconnected', () => {
    console.log('Mongoose disconnected');
});

mongoose.connection.on('error', (err: any) => {
    console.log('Mongoose connection error' + err);
});

process.on('SIGINT', () => {
    mongoose.connection.close(() => {
        console.log('Mongoose disconnected through app termination (SIGINT)');
        process.exit(0);
    });
});

process.on('SIGTERM', () => {
    mongoose.connection.close(() => {
        console.log('Mongoose disconnected through app termination (SIGTERM)');
        process.exit(0);
    });
});

process.once('SIGUSR2', () => {
    mongoose.connection.close(() => {
        console.log('Mongoose disconnected through app termination (SIGUSR2)');
        process.kill(process.pid, 'SIGUSR2');
    });
});

/server/models/user.ts

import * as mongoose from 'mongoose';
const Schema = mongoose.Schema;
const mongooseUniqueValidator = require('mongoose-unique-validator');

export interface IUser extends mongoose.Document {
    firstName: string,
    lastName: string,
    city: string,
    state: string,
    password: string,
    email: string,

    books: Array<{
        book: any, 
        onLoan: boolean, 
        loanedTo: any 
    }>
}

const schema = new Schema({
    firstName: { type: String, required: true },
    lastName: { type: String, required: true },
    city: { type: String, required: true },
    state: { type: String, required: true },
    password: { type: String, required: true },
    email: { type: String, required: true, unique: true },

    books: [{ 
        book: { type: Schema.Types.ObjectId, ref: 'Book', required: true},
        onLoan: { type: Boolean, required: true },
        loanedTo: { type: Schema.Types.ObjectId, ref: 'User'}
    }]
});

schema.plugin(mongooseUniqueValidator);

export const User = mongoose.model<IUser>('User', schema);

/server/routes/api.ts

import * as express from 'express';
const router = express.Router();

import { userRoutes } from './user';


/* GET api listing. */
router.use('/user', userRoutes);

export { router as api };

/server/routes/user.ts

import * as express from 'express';
const router = express.Router();
import * as bcrypt from 'bcryptjs';

import { User } from '../models/user';

router.post('/', function (req, res, next) {
    bcrypt.hash(req.body.password, 10)
        .then((hash) => {
            const user = new User({
                firstName: req.body.firstName,
                lastName: req.body.lastName,
                city: req.body.city,
                state: req.body.state,
                password: hash,
                email: req.body.email
            });
            return user.save();
        })
        .then((user) => {
            res.status(201).json({
                message: 'User created',
                obj: user
            });
        })
        .catch((error) => {
            res.status(500).json({
                title: 'An error occured',
                error: error
            });
        });
});

/server/routes/user.spec.ts

import * as request from 'supertest';
import * as td from 'testdouble';
import { server } from '../../server';
import { finishTest } from '../../spec/helpers/suptertest';


describe('user route', function () {
  let app: any;
  beforeEach(function () {
    app = server;
  });
  afterEach(function (done) {
    app.close(done);
  });
  it('creates a user /', (done) => {
    //make request
    request(app)
      .post('/api/user')
      .send({
        firstName: 'Philippe',
        lastName: 'Vaillancourt',
        city: 'Laval',
        state: 'Qc',
        password: 'test',
        email: 'test@test.com',
      })
      .expect(201, finishTest(done));
  });

});

I use supertest to fake the requests and use Jasmine as a test framework and runner.

My question: What do I need to change in my spec file in order to get this test to bypass making a call to the database and instead use stubs or mocks?

snowfrogdev
  • 5,963
  • 3
  • 31
  • 58
  • Is it `Unit Test` or `Integration Test`? – Fazal Rasel Jul 13 '17 at 16:07
  • Well, that's a good question. I'm testing more than a single function, I guess. So maybe it should be considered an integration test. What I'm trying to do is test that router.post('/') does what it is suppose to do, which is take the request's body make a call to the database to create a new user, and then send the proper response. Except I don't want the test to make an actual call to the database. I wan't to fake that part. – snowfrogdev Jul 13 '17 at 16:34

3 Answers3

4

I believe the answer you are looking for can be found at this video: Unit Testing Express Middleware / TDD with Express and Mocha

I have decided to follow It's instructions and It has been great so far. The thing is to split your routes between routes and middlewares, so you can test your business logic without calling or starting a server. Using node-mocks-http you can mock the request and response params.

To mock my models calls I am using sinon to stub methods like get, list and stuff that should hit the database. For your case the same video will provide an example of using mockgoose.

A simple example could be:

/* global beforeEach afterEach describe it */

const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')
const sinon = require('sinon')
const httpMocks = require('node-mocks-http')
const NotFoundError = require('../../app/errors/not_found.error')
const QuestionModel = require('../../app/models/question.model')
const QuestionAdminMiddleware = require('../../app/middlewares/question.admin.middleware')

chai.use(chaiAsPromised)
const expect = chai.expect
let req
let res

beforeEach(() => {
  req = httpMocks.createRequest()
  res = httpMocks.createResponse()
  sinon.stub(QuestionModel, 'get').callsFake(() => {
    return new Promise((resolve) => {
      resolve(null)
    })
  })
})

afterEach(() => {
  QuestionModel.list.restore()
  QuestionModel.get.restore()
})

describe('Question Middleware', () => {
  describe('Admin Actions', () => {
    it('should throw not found from showAction', () => {
      return expect(QuestionAdminMiddleware.showAction(req, res))
              .to.be.rejectedWith(NotFoundError)
    })
  })
})

At this example I wanna simulate a not found error but you can stub wherever return you may need to suit your middleware test.

  • Could you give a code example of the way you mock your models calls by using sinon to stub methods like get, list and stuff that should hit the database. – snowfrogdev Jul 13 '17 at 20:38
3

Jasmine makes mocking things pretty simple using spies. The first thing to do would be to use Model.create instead of the new keyword, then you can spy on the model methods and override their behavior to return a mock.

// Import model so we can apply spies to it...
import {User} from '../models/user';

// Example mock for document creation...
it('creates a user', (done) => {

    let user = {
        firstName: 'Philippe',
        lastName: 'Vaillancourt',
        city: 'Laval',
        state: 'Qc',
        password: 'test',
        email: 'test@test.com'
    };

    spyOn(User, 'create').and.returnValue(Promise.resolve(user));

    const request = {
        firstName: 'Philippe',
        lastName: 'Vaillancourt',
        city: 'Laval',
        state: 'Qc',
        password: 'test',
        email: 'test@test.com'
    };
    request(app)
        .post('/api/user')
        .send(request)
        .expect(201)
        .end((err) => {
            expect(User.create).toHaveBeenCalledWith(request);

            if (err) {
                return done(err);
            }
            return done();
        });
});

// Example mock for document querying...
it('finds a user', (done) => {

    let user = {
        firstName: 'Philippe',
        lastName: 'Vaillancourt',
        city: 'Laval',
        state: 'Qc',
        password: 'test',
        email: 'test@test.com'
    };

    let query = jasmine.createSpyObj('Query', ['lean', 'exec']);
    query.lean.and.returnValue(query);
    query.exec.and.returnValue(Promise.resolve(user));

    spyOn(User, 'findOne').and.returnValue(query);

    request(app)
        .get('/api/user/Vaillancourt')
        .expect(200)
        .end((err) => {
            expect(User.findOne).toHaveBeenCalledWith({lastName: 'Vaillancourt'});
            expect(query.lean).toHaveBeenCalled();
            expect(query.exec).toHaveBeenCalled();

            if (err) {
                return done(err);
            }
            return done();
        });
});
Jake Holzinger
  • 5,783
  • 2
  • 19
  • 33
0

Use sinon.js to stub your models.

var sinon = require('sinon');
var User = require('../../application/models/User');


it('should fetch a user', sinon.test(function(done) {
  var stub = this.stub(User, 'findOne', function(search, fields, cb) {
      cb(null, {
        _id: 'someMongoId',
        name: 'someName'
      });
  });

  // mocking an instance method
  // the `yields` method calls the supplied callback with the arguments passed to it
  this.stub(User.prototype, 'save').yields(null, {
        _id: 'someMongoId',
        name: 'someName'
  });

  // make an http call to the route that uses the User model. 
  // the  findOne method in that route will now return the stubbed result 
  // without making a call to the database
  // call `done();` when you are finished testing
}));

Notes:

  1. Because we are using sinon.test syntax, you don't have to worry about resetting the stubs.
Lucky Soni
  • 6,811
  • 3
  • 38
  • 57