Unit testing Express/Mongoose app routes without hitting the database
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.expectlet reqlet resbeforeEach(() => { 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.
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(); });});
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:
- Because we are using
sinon.test
syntax, you don't have to worry about resetting the stubs.