I am using fastify framework for my node.js application and sequelize as an ORM. I am using mocha, chai, and Sinon for unit testing. I have to unit test my controller function. The following is the sample controller function.
// controllers.js;
const service = require('../services');
exports.create = (req, reply) => {
const attributes = req.body;
service.create(attributes)
.then((result) => {
reply.code(201).send(result);
})
.catch((error) => {
reply.send(error);
});
};
and my services file is as follows,
// services.js;
const { Model } = require('../models');
function create(attributes) {
return Model.create(attributes);
}
module.exports = { create };
In the above code, I want to unit test only the 'create' function in controllers.js. The problem is, it should not call the database, since it is unit testing. But the Model.create in service.js file will make a call to the database. How can I unit test controller function only?
You should stub service.create method and create mocked req, reply objects.
E.g.
controller.js:
const service = require('./service');
exports.create = (req, reply) => {
const attributes = req.body;
service
.create(attributes)
.then((result) => {
reply.code(201).send(result);
})
.catch((error) => {
reply.send(error);
});
};
service.js:
const { Model } = require('./models');
function create(attributes) {
return Model.create(attributes);
}
module.exports = { create };
models.js:
const Model = {
create() {
console.log('real implementation');
},
};
module.exports = { Model };
controller.test.js:
const controller = require('./controller');
const service = require('./service');
const sinon = require('sinon');
const flushPromises = () => new Promise(setImmediate);
describe('62536251', () => {
afterEach(() => {
sinon.restore();
});
it('should create', async () => {
const mResult = 'success';
sinon.stub(service, 'create').resolves(mResult);
const mReq = { body: {} };
const mReply = { code: sinon.stub().returnsThis(), send: sinon.stub() };
controller.create(mReq, mReply);
await flushPromises();
sinon.assert.calledWith(mReply.code, 201);
sinon.assert.calledWith(mReply.send, 'success');
});
it('should handle error', async () => {
const mError = new Error('network');
sinon.stub(service, 'create').rejects(mError);
const mReq = { body: {} };
const mReply = { code: sinon.stub().returnsThis(), send: sinon.stub() };
controller.create(mReq, mReply);
await flushPromises();
sinon.assert.calledWith(mReply.send, mError);
});
});
unit test result with coverage report:
62536251
✓ should create
✓ should handle error
2 passing (13ms)
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 83.33 | 100 | 60 | 83.33 |
controller.js | 100 | 100 | 100 | 100 |
models.js | 66.67 | 100 | 0 | 66.67 | 3
service.js | 66.67 | 100 | 0 | 66.67 | 4
---------------|---------|----------|---------|---------|-------------------
Related
I am using Jest for unit testing my code.
I have a file userinfo.js
const getuserdata = async function (req, res, next) {
let userinfo = await getUserInfo();
return userinfo;
}
const getUserInfo = async function (req, res, next) {
const records = await database.query('select * from users');
return records;
}
module.exports = {
getuserdata,
getUserInfo
}
I want to write test in jest
I created a file userinfo.test.js for testing.
code:
const { getuserdata, getUserInfo } = require('./userinfo');
describe('user data', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('user info', async () => {
jest.mock('Path of file', () => ({ getUserInfo: jest.fn() }));
getUserInfo.mockResolvedValue([{id: 1, name: "test", password: "123456"}]);
const userdata = await getuserdata();
expect(userdata).not.toBeUndefined();
});
}
I want to test getuserdata but mock getUserInfo.
When I try to mock by above method it is giving me error that: TypeError: getUserInfo is not a function
but if I save the getUserInfo in different file then mocking is working properly. can you please suggest me how to fix this issue.
You need to do some refactoring. We need to make sure getUserInfo method has the same reference, then you can add spy on it or replace it with a mocked object. You should NOT use jest.mock in the function scope. It should be used in the module scope. This scenario is more appropriate to use jest.spyOn to mock getUserInfo method and its returned value.
E.g.
userinfo.js:
const database = require('./database');
const getuserdata = async function (req, res, next) {
let userinfo = await exports.getUserInfo();
return userinfo;
};
const getUserInfo = async function (req, res, next) {
const records = await database.query('select * from users');
return records;
};
exports.getuserdata = getuserdata;
exports.getUserInfo = getUserInfo;
database.js:
const database = {
query(sql) {
console.log('real implementation');
},
};
module.exports = database;
userinfo.test.js:
const userInfo = require('./userinfo');
describe('62539321', () => {
it('should pass', async () => {
const getUserInfoSpy = jest
.spyOn(userInfo, 'getUserInfo')
.mockResolvedValue([{ id: 1, name: 'test', password: '123456' }]);
const userdata = await userInfo.getuserdata();
expect(userdata).toEqual([{ id: 1, name: 'test', password: '123456' }]);
getUserInfoSpy.mockRestore();
});
});
unit test result with coverage report:
62539321
✓ should pass (7ms)
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 71.43 | 50 | 33.33 | 75 |
database.js | 66.67 | 100 | 0 | 66.67 | 3
userinfo.js | 72.73 | 50 | 50 | 77.78 | 9-10
-------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 13.609s
I'm a beginner with JS tests and I'm having issues when I try to mockup the value of a constant in the file I need to test.
I have the following file
// index.js
const { MultiAccounts } = require('../../some.js')
const MultiAccountInstance = new MultiAccounts();
...
const syncEvents = () => Promise.try(() => {
// ...
return MultiAccountInstance.all()
.then((accounts) => // ...); // ==> it throws the exception here Cannot read property 'then' of undefined
});
module.exports = syncEvents;
So, I will like to mockup the MultiAccountInstance constant.
I had been trying using Simon and rewire, but with the following script I'm having it throws the exception here Cannot read property 'then' of undefined exception in the script above.
//index.test.js
const rewire = require('rewire');
const indexRewired = rewire('.../../index/js');
describe('testing sync events', () => {
let fakeMultiAccountInstance, MultiAccountInstanceReverter;
let accounts;
beforeEach(() => {
accounts = [{id: 1}, {id: 2}];
fakeMultiAccountInstance = {};
fakeMultiAccountInstance.all = () => Promise.resolve(accounts);
MultiAccountInstanceReverter = indexRewired.__set__('MultiAccountInstance', fakeMultiAccountInstance);
});
afterEach(() => {
MultiAccountInstanceReverter();
});
it('testing', ()=> {
const spy = sinon.stub(fakeMultiAccountInstance, 'all');
return indexRewired().then((resp) => {
spy.restore();
expect(spy).to.have.been.calledWith({someParams: true});
});
})
});
How can I achieve this?. I also tried using stubs, but I'm having the error that the MultiAccountInstance.all is not a function
it's something like this
//index.test.js
const rewire = require('rewire');
const indexRewired = rewire('.../../index/js');
describe('testing sync events', () => {
let stubMultiAccountInstance, MultiAccountInstanceReverter;
let accounts;
beforeEach(() => {
accounts = [{id: 1}, {id: 2}];
stubMultiAccountInstance= sinon.stub().returns({
all: () => Promise.resolve(accounts), // also tried with sinon.stub().resolves(accounts)
});
MultiAccountInstanceReverter = indexRewired.__set__('MultiAccountInstance', stubMultiAccountInstance);
});
afterEach(() => {
stubMultiAccountInstance.reset();
MultiAccountInstanceReverter();
});
it('testing', ()=> {
return indexRewired().then((resp) => {
expect(stubMultiAccountInstance).to.have.been.calledWith({someParams: true});
});
})
});
Do you know what am I doing wrong?
Here is the unit test solution:
index.js:
const { MultiAccounts } = require('./some.js');
const Promise = require('bluebird');
let MultiAccountInstance = new MultiAccounts();
const syncEvents = () =>
Promise.try(() => {
return MultiAccountInstance.all().then((accounts) => console.log(accounts));
});
module.exports = syncEvents;
some.js:
function MultiAccounts() {
async function all() {}
return {
all,
};
}
module.exports = { MultiAccounts };
index.test.js:
const sinon = require('sinon');
const rewire = require('rewire');
const Promise = require('bluebird');
describe('61659908', () => {
afterEach(() => {
sinon.restore();
});
it('should pass', async () => {
const promiseTrySpy = sinon.spy(Promise, 'try');
const logSpy = sinon.spy(console, 'log');
const indexRewired = rewire('./');
const accounts = [{ id: 1 }, { id: 2 }];
const fakeMultiAccountInstance = {
all: sinon.stub().resolves(accounts),
};
indexRewired.__set__('MultiAccountInstance', fakeMultiAccountInstance);
await indexRewired();
sinon.assert.calledOnce(fakeMultiAccountInstance.all);
sinon.assert.calledWith(logSpy, [{ id: 1 }, { id: 2 }]);
sinon.assert.calledOnce(promiseTrySpy);
});
});
unit test results with coverage report:
61659908
[ { id: 1 }, { id: 2 } ]
✓ should pass (54ms)
1 passing (62ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 80 | 100 |
index.js | 100 | 100 | 100 | 100 |
some.js | 100 | 100 | 50 | 100 |
----------|---------|----------|---------|---------|-------------------
Please consider the following scenario, but I could not solve it. A class are called under the router("/"). I would like to test router ("/") call and mock myFunc result.
class A {
myFunc = async () => {
await ...
return result
}
}
Controller file - C:
router.get("/", async (req, res) => {
var a = new A();
a.myFunc()
.then(result => {
res.json({"message": result});
}).catch(e => {
return []
})
});
C.test.js:
const request = require("supertest");
const app = require("../C");
jest.mock('A');
test("Test route /", async done => {
const myFuncMock = jest.fn().mockImplementation(() => [...]);
A.prototype.myFunc = myFuncMock;
await request(app)
.get("/")
.then(res => {
expect(res.status).toBe(200); // Not asserted
});
done()
});
I could not success to mock function results under the router.
Here is the integration test solution:
app.js:
const express = require('express');
const { Router } = require('express');
const A = require('./a');
const app = express();
const router = Router();
router.get('/', async (req, res) => {
const a = new A();
a.myFunc()
.then((result) => {
res.json({ message: result });
})
.catch((e) => {
return [];
});
});
app.use(router);
module.exports = app;
a.js:
class A {
myFunc = async () => {
const result = await 'real data';
return result;
};
}
module.exports = A;
app.test.js:
const app = require('./app');
const request = require('supertest');
const A = require('./a');
jest.mock('./a', () => {
const mA = { myFunc: jest.fn() };
return jest.fn(() => mA);
});
describe('61505692', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should pass', () => {
const mA = new A();
mA.myFunc.mockResolvedValueOnce('fake result');
return request(app)
.get('/')
.then((res) => {
expect(res.status).toBe(200);
});
});
});
integration test results with coverage report:
PASS stackoverflow/61505692/app.test.js (11.834s)
61505692
✓ should pass (34ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 92.31 | 100 | 75 | 91.67 |
app.js | 92.31 | 100 | 75 | 91.67 | 16
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 13.785s
I need to test that my POST /login handler calling validServiceURL function inside itself.
I tried to solve this issue using spy from sinon, but didn't manage it.
POST /login handler:
const login = async (req, res) => {
try {
if (await validServiceURL(req.headers.host)) {
await checkUser(req, res);
} else {
res.redirect("/");
}
} catch (err) {
reditectToHomePageWithError(res, err.message);
}
};
Test code:
const chai = require("chai");
const sinon = require("sinon");
const request = require("supertest");
chai.use(require("sinon-chai"));
const expect = chai.expect;
const app = require("../../app");
const validServiceURL = require("../../lib/validServiceURL");
const sandbox = sinon.createSandbox();
describe("User", function() {
describe("POST login", function() {
it("should call validServiceURL with provided serviceURL", async () => {
let args = { username: user.username, password: password };
let serviceURL = "http://127.0.0.1:3030";
let spyValidServiceURL = sandbox.spy(validServiceURL);
await request(app)
.post("/login")
.type("form")
.send(args)
.set("host", serviceURL);
expect(spyValidServiceURL).to.have.been.calledWith(serviceURL);
sandbox.restore();
});
});
});
When I run this test I get error look like:
AssertionError: expected validServiceURL to have been called with arguments http://127.0.0.1:3030
sinon doesn't support stub/spy a standalone function directly. In your case, the standalone function is validServiceURL. For more info, see this issue.
Here is the solution using proxyquire module:
app.js:
const express = require("express");
const validServiceURL = require("./validServiceURL");
const app = express();
async function checkUser(req, res) {
console.log("checkUser");
}
function reditectToHomePageWithError(res, message) {
console.log(message);
}
const login = async (req, res) => {
try {
if (await validServiceURL(req.headers.host)) {
await checkUser(req, res);
} else {
res.redirect("/");
}
} catch (err) {
reditectToHomePageWithError(res, err.message);
}
};
app.post("/login", login);
module.exports = app;
validServiceURL.js:
async function validServiceURL() {}
module.exports = validServiceURL;
app.test.js:
const chai = require("chai");
const sinon = require("sinon");
const request = require("supertest");
const proxyquire = require("proxyquire");
chai.use(require("sinon-chai"));
const expect = chai.expect;
const sandbox = sinon.createSandbox();
describe("User", function() {
describe("POST login", function() {
it("should call validServiceURL with provided serviceURL", async () => {
let args = { username: "username", password: "password" };
let serviceURL = "http://127.0.0.1:3030";
let spyValidServiceURL = sandbox.spy();
const app = proxyquire("./app", {
"./validServiceURL": spyValidServiceURL,
});
await request(app)
.post("/login")
.type("form")
.send(args)
.set("host", serviceURL);
expect(spyValidServiceURL).to.have.been.calledWith(serviceURL);
sandbox.restore();
});
});
});
Integration test result with coverage:
User
POST login
✓ should call validServiceURL with provided serviceURL (468ms)
1 passing (474ms)
--------------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
--------------------|----------|----------|----------|----------|-------------------|
All files | 87.1 | 50 | 57.14 | 87.1 | |
app.js | 69.23 | 50 | 33.33 | 69.23 | 6,10,16,21 |
app.test.js | 100 | 100 | 100 | 100 | |
validServiceURL.js | 100 | 100 | 0 | 100 | |
--------------------|----------|----------|----------|----------|-------------------|
Source code: https://github.com/mrdulin/mocha-chai-sinon-codelab/tree/master/src/stackoverflow/57908094
For some reason I am having a little trouble getting this simple test to run correctly with a similar set-up I have used multiple times before.
Perhaps a fresh pair of eyes could help me understand why my method generateReport is not being invoked and none of my stubs are being triggered with the intended arguments?
BYE nor GOOD are ever logged and the tests are returning the error: AssertError: expected stub to be called with arguments
My index file:
const errorHandler = require('./lib/handlers/error-handler')
const transformRequest = require('./lib/handlers/request-converter')
const convert = require('./lib/convert')
exports.generateReport = function generateReport(req, res) {
console.log('HELLO')
const objectToPopulateTemplate = transformRequest(req.body)
convert(objectToPopulateTemplate, function (e, data) {
if (e) {
console.log('BYE')
const error = errorHandler(e)
return res.send(error.httpCode).json(error)
}
console.log('GOOD')
res
.set('Content-Type', 'application/pdf')
.set('Content-Disposition', `attachment; filename=velocity_report_${new Date()}.pdf`)
.set('Content-Length', data.length)
.status(200)
.end(data)
})
}
My test file:
const proxyquire = require('proxyquire')
const assert = require('assert')
const sinon = require('sinon')
const fakeData = require('./data/sample-request.json')
describe('report-exporter', () => {
describe('generateReport', () => {
const fakeError = new Error('Undefined is not a function')
let res, resSendStub, resStatusStub, resEndStub, resSetStub, resJsonStub, req, convertStub, generate
before(() => {
resSendStub = sinon.stub()
resJsonStub = sinon.stub()
resStatusStub = sinon.stub()
resEndStub = sinon.stub()
resSetStub = sinon.stub()
convertStub = sinon.stub()
res = {
send: function(errorCode) {
return resSendStub(errorCode)
},
json: function(object) {
return resJsonStub(object)
},
set: function(arg1, arg2) {
return resSetStub(arg1, arg2)
},
status: function(code) {
return resStatusStub(code)
},
end: function(data) {
return resEndStub(data)
}
}
req = {
body: {}
}
generate = proxyquire('./../index', {
'./lib/convert': function() {
return convertStub
}
})
})
it('Should return an error response', () => {
convertStub.throws(fakeError)
generate.generateReport(req, res)
sinon.assert.calledWith(resSendStub, '500')
})
})
})
It looks like you are proxyquireing your ./lib/convert incorrectly. Original convert is called with objectToPopulateTemplate and a callback function (e, data). And that's the callback who is responsible for error handling and sending a response.
The stubbed convert function though doesn't care about about the provided callback at all, so the callback never gets called.
Most likely what you want is to pass the parameters to convertStub and deal with them later:
'./lib/convert': function(objectToPopulateTemplate, cb) {
return convertStub(objectToPopulateTemplate, cb);
}
and then
it('Should return an error response', () => {
generate.generateReport(req, res);
const cb = convertStub.getCall(0).args[1];
// simulate `convert` to fail
cb(fakeError);
sinon.assert.calledWith(resSendStub, '500')
})
Here is the unit test solution:
index.js:
const errorHandler = require("./error-handler");
const transformRequest = require("./request-converter");
const convert = require("./convert");
exports.generateReport = function generateReport(req, res) {
console.log("HELLO");
const objectToPopulateTemplate = transformRequest(req.body);
convert(objectToPopulateTemplate, function(e, data) {
if (e) {
console.log("BYE");
const error = errorHandler(e);
return res.send(error.httpCode).json(error);
}
console.log("GOOD");
res
.set("Content-Type", "application/pdf")
.set(
"Content-Disposition",
`attachment; filename=velocity_report_${new Date()}.pdf`
)
.set("Content-Length", data.length)
.status(200)
.end(data);
});
};
error-handler.js:
module.exports = function(error) {
return error;
};
request-converter.js:
module.exports = function transformRequest(body) {
return body;
};
convert.js:
module.exports = function convert(body, callback) {
callback();
};
index.spec.js:
const sinon = require("sinon");
const proxyquire = require("proxyquire");
describe("report-exporter", () => {
describe("generateReport", () => {
afterEach(() => {
sinon.restore();
});
const fakeError = new Error("Undefined is not a function");
fakeError.httpCode = 500;
it("Should return an error response", () => {
const logSpy = sinon.spy(console, "log");
const mReq = { body: {} };
const mRes = { send: sinon.stub().returnsThis(), json: sinon.stub() };
const convertStub = sinon.stub();
const errorHandlerStub = sinon.stub().returns(fakeError);
const transformRequestStub = sinon.stub().returns(mReq.body);
const generate = proxyquire("./", {
"./convert": convertStub,
"./error-handler": errorHandlerStub,
"./request-converter": transformRequestStub
});
generate.generateReport(mReq, mRes);
convertStub.yield(fakeError, null);
sinon.assert.calledWith(transformRequestStub);
sinon.assert.calledWith(convertStub, {}, sinon.match.func);
sinon.assert.calledWith(errorHandlerStub, fakeError);
sinon.assert.calledWith(logSpy.firstCall, "HELLO");
sinon.assert.calledWith(logSpy.secondCall, "BYE");
});
});
});
Unit test result with coverage report:
report-exporter
generateReport
HELLO
BYE
✓ Should return an error response (94ms)
1 passing (98ms)
----------------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------------------|----------|----------|----------|----------|-------------------|
All files | 87.5 | 50 | 62.5 | 87.5 | |
convert.js | 50 | 100 | 0 | 50 | 2 |
error-handler.js | 50 | 100 | 0 | 50 | 2 |
index.js | 84.62 | 50 | 100 | 84.62 | 14,15 |
index.spec.js | 100 | 100 | 100 | 100 | |
request-converter.js | 50 | 100 | 0 | 50 | 2 |
----------------------|----------|----------|----------|----------|-------------------|
Source code: https://github.com/mrdulin/mocha-chai-sinon-codelab/tree/master/src/stackoverflow/47352972