Mocking mongoose save() error inside a route - javascript

After trying all manner of methods to test a route's mongoose save() throwing, I was not really sure how it should be done. I'm aiming for 100 % coverage with istanbul. Here's the core setup:
model.js
let mongoose = require('mongoose');
let Schema = mongoose.Schema;
let PasteSchema = new Schema(
{
message: { type: String, required: true },
tags: [String],
marked: { type: Boolean, default: false },
createdAt: { type: Date, default: Date.now },
updatedAt: Date
}
);
module.exports = mongoose.model('paste', PasteSchema);
controller.js
let Paste = require('./model');
// Other stuff
// I use a bit non-standard DELETE /pastes/:id for this
const markPaste = (req, res) => {
Paste.findById({ _id: req.params.id }, (err, paste) => {
if (!paste) {
res.status(404).json({ result: 'Paste not found' });
return;
}
paste.marked = true;
paste.updatedAt = new Date();
paste.save((err) => {
err
? res.status(400).json({ result: err })
: res.json({ result: 'Paste marked' });
});
});
}
module.exports = {
markPaste,
// Other stuff
}
routes.js
const express = require('express');
const app = express();
const pastes = require('./apps/pastes/controller'); // The file above
app.route('/pastes/:id')
.delete(pastes.markPaste);
module.exports = app;
In the below test, I want to simulate an error being thrown in the paste.save((err) => { above.
process.env.NODE_ENV = 'test';
let mongoose = require('mongoose');
let Paste = require('../apps/pastes/model');
let server = require('../index');
let chai = require('chai');
chai.use(require('chai-http'));
chai.use(require('chai-date-string'));
let expect = chai.expect;
let sinon = require('sinon');
let sandbox = sinon.createSandbox();
let pastes = require('../apps/pastes/controller');
let httpMocks = require('node-mocks-http');
// Other tests
Then the test I want to test save() error in the route:
it('should handle an error during the save in the endpoint', (done) => {
// Create a paste to be deleted
const pasteItem = new Paste({ message: 'Test 1', tags: ['integration', 'test'] });
pasteItem.save()
.then((paste) => {
// Attempt code from below goes here
})
.catch((err) => {
console.log('Should not go here');
});
done();
});

I didn't really find any clear reference to this in various Stack questions, or online, so here's how I did it:
The secret is in using the sinon sandbox, which applies even inside the route context during tests. Here is the working test:
it('should handle an error during the save in the endpoint', (done) => {
const pasteItem = new Paste({ message: 'Test 1', tags: ['integration', 'test'] });
pasteItem.save()
.then((paste) => {
// the sandbox is defined in the imports
// This forces calling save() to raise an error
sandbox.stub(mongoose.Model.prototype, 'save').yields({ error: 'MongoError' });
chai.request(server)
.delete('/pastes/' + paste._id)
.end((err, res) => {
// It applies within the route handling, so we get the expected 400
expect(res).to.have.status(400);
done();
});
})
.catch((err) => {
console.log('Should not go here');
});
});
If you would call it outside of the sandbox, you would break all subsequent tests that use sinon. Ie.
// This would break things unintendedly
sinon.stub(mongoose.Model.prototype, 'save').yields({ error: 'MongoError' });
// This only breaks things (on purpose) in the test we want it to break in:
sandbox.stub(mongoose.Model.prototype, 'save').yields({ error: 'MongoError' });
If you have multiple things within the particular sandbox instance, you can of course restore the "unbroken" state within the test with sandbox.restore(); after the test case.
->
=============================== Coverage summary ===============================
Statements : 100% ( 60/60 )
Branches : 100% ( 14/14 )
Functions : 100% ( 0/0 )
Lines : 100% ( 57/57 )
================================================================================
Yay!

Related

Extracting and testing an express-validator function

I'd like to test my implementation of express-validator rules in my middleware. I understand that I shouldn't test 3rd party code, but in my (perhaps flawed) view, I'm testing my implementation rather than their code.
A cut down version would usually look like this:
// routes.js
router.post('/example',
[
body('email')
.isString()
.withMessage('Invalid characters, please use letters and numbers only'))
],
//// To be replaced by:
// validateLogin(),
controller.exampleFn());
but I need to be able to extract it for testing, which I do by running the validations imperatively:
// validation.js
// parallel processing
const validate = validations => {
return async (req, res, next) => {
await Promise.all(validations.map(validation => validation.run(req)));
const errors = validationResult(req);
if (errors.isEmpty()) {
return next();
}
const error = new Error();
error.message = process.env.NODE_ENV === 'development'? 'Validation Failed':'Error';
error.statusCode = !errors.isEmpty()? 422:500;
error.errors = errors.array({onlyFirstError: true});
next(error);
return error;
};
};
const validateLogin = () => {
const exampleValidationRules = [
body('email')
.isString()
.withMessage('Invalid characters, please use letters and numbers only')
];
return validation(exampleValidationRules);
}
module.exports = {
validateLogin
};
I can then call the middleware in my routes, and in my test files.
For example:
// auth.test.js
describe('Unit Tests', () => {
it('should return 422 if email validation fails', async() => {
const wrongEmailReq = { body: {email: 'nic#hotmail.com'} };
const notStringReq = { body: {email:1} };
const res = {
statusCode: 500,
status: (code) => {this.statusCode = code; return this},
};
// Function to be tested
const validationFn = validateLogin();
const wrongEmail = await validationFn(wrongEmailReq, res, ()=>{});
const notString = await validationFn(notStringReq, res, ()=>{});
expect(wrongEmail.statusCode).to.be.equal(422);
expect(wrongEmail.errors[0].param).to.be.equal('email');
expect(notString.statusCode).to.be.equal(422);
expect(notString.errors[0].param).to.be.equal('email');
return;
});
I'm just slightly confused about how I'd test the 'success' case. validationFn returns next() if successful. But that would just be undefined.
Should I just test expect(correct.statusCode).to.be.undefined;? That doesn't seem specific enough.
Also, is there actually any advantage to unit testing this function over using http-chai to run the requests? I thought perhaps it was more lightweight, but unsure.

How to check return value in Jest

I'm doing unit testing with jest and was able to successfully run some of it but there's certain code that I don't know how to test.
I have Create Organization method that needs to check first if the organization is already exist.
async createOrganization(opt) {
try {
const organizationExist = await this.OrganizationRepository.getOne({name: opt.name})
if (organizationExist) {
throw new Error('Organization already exist')
}
} catch (error) {
throw error
}
let organizationObject = {}
organizationObject.name = opt.name
return this.OrganizationRepository.save(organizationObject)
}
and so far this is the unit test code that I was able to cover
describe('Create Organization', () => {
it('should call getOne function', () => {
const mockGetOne = jest.spyOn(OrganizationRepository.prototype, 'getOne')
organizationService.createOrganization(expectedOrganization)
expect(mockGetOne).toHaveBeenCalledWith({name: 'sample org'})
})
it('should return created organization', async () => {
const mockSave = jest.spyOn(OrganizationRepository.prototype, 'save')
mockSave.mockReturnValue(Promise.resolve(expectedOrganization))
const result = await organizationService.createOrganization({name: 'sample org'})
expect(mockSave).toHaveBeenCalledWith({name: 'sample org'})
expect(result).toBe(expectedOrganization)
})
})
now what I want to test is this part
const organizationExist = await this.OrganizationRepository.getOne({name: opt.name})
if (organizationExist) {
throw new Error('Organization already exist')
}
I want to throw an error if the organization is already exist using the name parameter.
Hope you guys can help me. Thanks
you could use toThrowError to test this scenario.
it("Should throw error", async () => {
const mockGetOne = jest.spyOn(OrganizationRepository.prototype, 'getOne')
await organizationService.createOrganization({ name: 'sample org' }); ;
expect(mockGetOne).toHaveBeenCalledWith({ name: 'sample org' });
// Test the exact error message
expect( organizationService.createOrganization({ name: 'sample org' }))
.resolves
.toThrowError(new Error("Organization already exist"));
});
Are you looking for toThrow()?
expect(() => someFunctionCall()).toThrow();

Sinon stub not working with module.exports = { f1, f2}

I have this file which sends otp like below.
OtpService.js
const generateOTP = async function() {
//
}
const verifyOTP = async function() {
//
}
module.exports = {
generateOTP,
verifyOTP
}
Below is the controller that uses these methods, otp.js
const { verifyOTP, generateOTP } = require('../../services/OtpService')
const verify = async function(req, res) {
const {error, data} = await generateOTP(req.query.phone)
}
const send = async function(req, res) {
const {error, data} = await verifyOTP(req.query.phone, req.query.otp)
}
module.exports = {
send,
verify
}
below is the test file otp.test.js
const sinon = require('sinon');
const expect = require('chai').expect
const request = require('supertest')
const OtpService = require('../../src/services/OtpService')
console.log(OtpService)
describe('GET /api/v1/auth/otp', function() {
let server
let stub
const app = require('../../src/app')
stub = sinon.stub(OtpService, 'generateOTP').resolves({
error: null,
data: "OTP Sent"
})
server = request(app)
it('should generate OTP', async () => {
const result = await server
.get('/api/v1/auth/otp/send?phone=7845897889')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200)
console.log(result.body)
expect(stub.called).to.be.true
expect(result).to.be.a('Object')
});
});
Above is not working, it's not stubbing the generateOTP and verifyOTP methods when called in the controller.
However, If I call OtpService.generateOTP() in the otp.test.js then it is working there, but it doesn't work in the controller.
Hhow sinon is working here?
I am confused here.
Does requiring the app and then stubbing is correct or stubbing first and then requiring is correct?
I have tried both ways though neither of them work.
I also tried using before() and beforeEach().
Below is my folder structure.
otp.js(controller) is here controller->AuthController->otp.js
otp.test.js is here test->auth->otp.test.js
OtpService.js is just inside services
Update
I found the the problem.
If I don't use destructing feature in the controller everything works fine. So, using OtpService.generateOTP works.
Problem is with the destructing of the objects.
const { verifyOTP, generateOTP } = require('../../services/OtpService')
Above is being run before the stubbing. So verifyOTP and generateOTP already has a reference to the unstubbed methods.
I need a workaround here. I want to use destructing feature.
I use proxyquire package to stub OtpService module. The below example is unit test, but you can use this way for your integration test.
E.g.
otp.js:
const { verifyOTP, generateOTP } = require('./OtpService');
const verify = async function(req, res) {
return generateOTP(req.query.phone);
};
const send = async function(req, res) {
return verifyOTP(req.query.phone, req.query.otp);
};
module.exports = {
send,
verify,
};
OtpService.js:
const generateOTP = async function() {
//
};
const verifyOTP = async function() {
//
};
module.exports = {
generateOTP,
verifyOTP,
};
otp.test.js:
const proxyquire = require('proxyquire');
const sinon = require('sinon');
describe('60704684', () => {
it('should send', async () => {
const otpServiceStub = {
verifyOTP: sinon.stub().resolves({ error: null, data: 'fake data' }),
generateOTP: sinon.stub(),
};
const { send } = proxyquire('./otp', {
'./OtpService': otpServiceStub,
});
const mReq = { query: { phone: '123', otp: 'otp' } };
const mRes = {};
await send(mReq, mRes);
sinon.assert.calledWithExactly(otpServiceStub.verifyOTP, '123', 'otp');
});
it('should verfiy', async () => {
const otpServiceStub = {
verifyOTP: sinon.stub(),
generateOTP: sinon.stub().resolves({ error: null, data: 'fake data' }),
};
const { verify } = proxyquire('./otp', {
'./OtpService': otpServiceStub,
});
const mReq = { query: { phone: '123' } };
const mRes = {};
await verify(mReq, mRes);
sinon.assert.calledWithExactly(otpServiceStub.generateOTP, '123');
});
});
unit test results with coverage report:
60704684
✓ should send (1744ms)
✓ should verfiy
2 passing (2s)
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 50 | 100 |
OtpService.js | 100 | 100 | 0 | 100 |
otp.js | 100 | 100 | 100 | 100 |
---------------|---------|----------|---------|---------|-------------------
source code: https://github.com/mrdulin/expressjs-research/tree/master/src/stackoverflow/60704684
The problem is when you import something try to stub any of its property, it actually stub the property of the imported object instead of the one you want to. which exist somewhere in other application.
I had similar issue, I moved my sendOtp function to Model. and import that model and stub sendOtp method.
It worked!
in your User model or schema, add
Models/Users.js
Users.generateOtp = async function(){ // your sms sending code}
tests/yourtestfile.js
const {Users} = require("../yourModelsDirectory");
const sinon = require('sinon');
desribe("your test case", function(){
const stub = sinon.stub(Users, "generateOtp"); // stub generateOtp method of Users model (obj)
stub.returns(true); // make it do something
// your tests
it("do something", function(){// test code})
it("do another thing", function(){// test code})
after() {
// resotre stubbed methods in last
sinon.restore();
}
})
I recommend adding stubbing and restore part in before() and after() hooks.
let me know if it need more clearification.

TypeError: Attempted to wrap undefined property find as function - NodeJs

I'm trying to run my test as follow using the TDD aproach. I have the same test running on another app (I copy paste it) and it works but in this test I get the following error:
TypeError: Attempted to wrap undefined property find as function
Model file
/*
Task Model Database models
*/
const mongoose = require('mongoose');
try {
module.exports = mongoose.model('Task');
} catch (error) {
const taskSchema = mongoose.Schema({
title: { type: String, required: true },
status: { type: Boolean, required: true }
});
module.exports = taskSchema;
}
/* Sample Test */
'use strict';
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const sinon = require('sinon');
const expect = chai.expect;
const Task = require('../../models/task');
chai.should();
chai.use(chaiAsPromised);
//TEST DRIVEN DEVELOPMENT
describe('Todo Controller', () => {
const expectedResult = { status: 201, tasks: [{}], message: '' };
//Testing if the array has a valid status
it('should get a valid status', done => {
const TodoMock = sinon.mock(Task);
TodoMock.expects('find').yields(null, expectedResult);
Task.find((err, result) => {
TodoMock.verify();
TodoMock.restore();
expect(result.status).to.be.equal(201);
done();
});
});
});
It looks like you have some scoping issues. I would write the model like this:
const mongoose = require('mongoose');
const taskSchema;
try {
taskSchema = mongoose.model('Task');
} catch (error) {
taskSchema = mongoose.Schema({
title: { type: String, required: true },
status: { type: Boolean, required: true }
});
}
module.exports = taskSchema;

I am trying to test a condition wherein the API returns 500 (internal server error) using mocha and chai

Please, I don't have much of idea of how to go about this. A good explanation of the process will be of great help. Thanks in anticipation.
Here is my controller
async getAllEntries(req, res) {
try {
const userId = req.userData.userID;
const query = await client.query(
`SELECT * FROM entries WHERE user_id=($1) ORDER BY entry_id ASC;`, [
userId,
],
);
const entries = query.rows;
const count = entries.length;
if (count === 0) {
return res.status(200).json({
message: 'There\'s no entry to display',
});
}
return res.status(200).json({
message: "List of all entries",
"Number of entries added": count,
entries,
});
} catch (error) {
return res.status(500).json({
message: "Error processing request",
error,
});
}
}
For this case, what I'm going to do is to make the client.query process failed. So, based on your code, it will go to catch statement.
const chai = require('chai');
const assert = chai.assert;
const sinon = require('sinon');
const client = require('...'); // path to your client library
const controller = require('...'); // path to your controller file
describe('controller test', function() {
let req;
let res;
// error object to be used in rejection of `client.query`
const error = new Error('something weird');
beforeEach(function() {
req = sinon.spy();
// we need to use `stub` for status because it has chain method subsequently
// and for `json` we just need to spy it
res = {
status: sinon.stub().returnsThis(),
json: sinon.spy()
};
// here we reject the query with specified error
sinon.stub(client, 'query').rejects(error);
});
afterEach(function() {
sinon.restore();
})
it('catches error', async function() {
await controller.getAllEntries(req, res);
// checking if `res` is called properly
assert(res.status.calledWith(500));
assert(res.json.calledWith({
message: 'Error processing request',
error
}));
});
});
Hope it helps.

Categories