Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit testing Express/Mongoose app routes without hitting the database

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: '[email protected]',
      })
      .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?

like image 403
snowfrogdev Avatar asked Jul 10 '17 23:07

snowfrogdev


People also ask

What is the difference between Mongoose and express?

Mongoose, by the way, is simply a layer of abstraction on top of MongoDB, the same way Express is a layer of abstraction on top of Node. We'll require Mongoose at the top of our JS file that starts the server and then invoke Mongoose's connect() method.

What is the purpose of unit testing?

The main objective of unit testing is to isolate written code to test and determine if it works as intended. Unit testing is an important step in the development process, because if done correctly, it can help detect early flaws in code which may be more difficult to find in later testing stages.

Is unit testing necessary?

Unit testing is an essential part of the software development process that helps you ensure the high quality of your product: it allows developers to check the performance of each unit and prevent possible problems in advance.


2 Answers

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.

like image 57
leandrorlemos Avatar answered Nov 15 '22 03:11

leandrorlemos


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: '[email protected]'
    };

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

    const request = {
        firstName: 'Philippe',
        lastName: 'Vaillancourt',
        city: 'Laval',
        state: 'Qc',
        password: 'test',
        email: '[email protected]'
    };
    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: '[email protected]'
    };

    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();
        });
});
like image 37
Jake Holzinger Avatar answered Nov 15 '22 05:11

Jake Holzinger