I'm trying to test a function which calls request.get() function inside it. What I'm trying is to cover all branches of the callback function. I'm trying to achieve it without separating the callback function to a different function because it's uses variables of the upper closure.
Here's an example to illustrate..
foo.js:
var request = require('request');
function func(arg1, arg2) {
request.get({...}, function(error, response, body) {
// do stuff with arg1 and arg2 // want to be covered
if (...) { // want to be covered
... // want to be covered
} else if (...) { // want to be covered
... // want to be covered
} else {
... // want to be covered
}
});
}
exports.func = func;
I tried to stub it with sinon as well as proxyquire.
foo.spec.js (stubbed with sinon):
var foo = require('./foo'),
var sinon = require('sinon'),
var request = require('request');
var requestStub = sinon.stub(request, 'get', function(options, callback) {
callback(new Error('Custom error'), {statusCode: 400}, 'body');
}); // Trying to replace it with a function that calls the callback immediately so not to deal with async operations in test
foo.func(5, 3);
foo.spec.js (stubbed with proxyquire):
var requestStub = {};
var proxyquire = require('proxyquire'),
var foo = proxyquire('./foo', {'request': requestStub}),
var sinon = require('sinon');
requestStub.get = function(options, callback) {
callback(new Error('Custom error'), {statusCode: 400}, 'body');
}; // Trying to replace it with a function that calls the callback immediately so not to deal with async operations in test
foo.func(5, 3);
Neither did work. When I tried to debug I never hit the callback function, which is suggesting that I didn't stub the request.get() method correctly making it still async operation.
I would appreciate someone tell me what I did wrong in both scenarios (sinon & proxyquire) and examples on how to fix it.
Here is an unit test solution using sinon and mocha:
index.js:
var request = require("request");
function func(arg1, arg2) {
request.get("https://github.com/mrdulin", function(error, response, body) {
console.log(arg1, arg2);
if (error) {
console.log(error);
} else if (response.status === 200) {
console.log(response);
} else {
console.log("others");
}
});
}
exports.func = func;
index.spec.js:
var foo = require("./");
var sinon = require("sinon");
var request = require("request");
describe("func", () => {
afterEach(() => {
sinon.restore();
});
it("should handle error", () => {
const logSpy = sinon.spy(console, "log");
const getStub = sinon.stub(request, "get");
foo.func(1, 2);
const mError = new Error("network error");
getStub.yield(mError, null, null);
sinon.assert.calledWith(
getStub,
"https://github.com/mrdulin",
sinon.match.func
);
sinon.assert.calledWith(logSpy.firstCall, 1, 2);
sinon.assert.calledWith(logSpy.secondCall, mError);
});
it("should handle response", () => {
const logSpy = sinon.spy(console, "log");
const getStub = sinon.stub(request, "get");
foo.func(1, 2);
const mResponse = { status: 200 };
getStub.yield(null, mResponse, null);
sinon.assert.calledWith(
getStub,
"https://github.com/mrdulin",
sinon.match.func
);
sinon.assert.calledWith(logSpy.firstCall, 1, 2);
sinon.assert.calledWith(logSpy.secondCall, mResponse);
});
it("should handle other situation", () => {
const logSpy = sinon.spy(console, "log");
const getStub = sinon.stub(request, "get");
foo.func(1, 2);
const mResponse = { status: 500 };
getStub.yield(null, mResponse, null);
sinon.assert.calledWith(
getStub,
"https://github.com/mrdulin",
sinon.match.func
);
sinon.assert.calledWith(logSpy.firstCall, 1, 2);
sinon.assert.calledWith(logSpy.secondCall, "others");
});
});
Unit test result with 100% coverage:
func
1 2
Error: network error
at Context.it (/Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/src/stackoverflow/37764363/index.spec.js:1:4103)
at callFn (/Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/node_modules/mocha/lib/runnable.js:387:21)
at Test.Runnable.run (/Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/node_modules/mocha/lib/runnable.js:379:7)
at Runner.runTest (/Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/node_modules/mocha/lib/runner.js:535:10)
at /Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/node_modules/mocha/lib/runner.js:653:12
at next (/Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/node_modules/mocha/lib/runner.js:447:14)
at /Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/node_modules/mocha/lib/runner.js:457:7
at next (/Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/node_modules/mocha/lib/runner.js:362:14)
at Immediate._onImmediate (/Users/ldu020/workspace/github.com/mrdulin/mocha-chai-sinon-codelab/node_modules/mocha/lib/runner.js:425:5)
at runCallback (timers.js:705:18)
at tryOnImmediate (timers.js:676:5)
at processImmediate (timers.js:658:5)
✓ should handle error (48ms)
1 2
{ status: 200 }
✓ should handle response
1 2
others
✓ should handle other situation
3 passing (58ms)
---------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
---------------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
index.js | 100 | 100 | 100 | 100 | |
index.spec.js | 100 | 100 | 100 | 100 | |
---------------|----------|----------|----------|----------|-------------------|
Source code: https://github.com/mrdulin/mocha-chai-sinon-codelab/tree/master/src/stackoverflow/37764363
Related
I'm testing the NodeJS function/module below using jestjs
const fs = require("fs");
const path = require("path");
function read(filename, callback) {
fs.readFile(path.join(__dirname, filename), { encoding: "utf-8" }, callback);
}
module.exports = {
read,
};
The first test is:
test("callback data returned is correct", (done) => {
function callback(err, data) {
try {
expect(data).toBe("1");
done();
} catch (err) {
done(err);
}
}
read("./test", callback);
});
and is succesful (I have an actual real test file with the contents being 1.
The second one is a bit trickier. Since I'm providing a mockCallback function to read, why isn't the callback called?
const { read } = require("./callback");
describe("callback testing", () => {
test("callback is called", (done) => {
const mockCallback = jest.fn((err, data) => data);
read("./test", mockCallback);
expect(mockCallback).toHaveBeenCalled();
done();
});
});
The test fails and while inspecting the mock function it seems it wasn't called.
I also tried adding the following in order to mock fs.readFile, without much success:
const mock = require("mock-fs");
const fs = require("fs");
jest.mock("fs");
beforeEach(() => {
mock({
test: "1",
});
});
I'd like a solution that mocks the least of the dependant methods and an explanation as to why isn't the callback run.
You should mock fs.readFile method and trigger the callback in your test case.
E.g.
callback.js:
const fs = require('fs');
const path = require('path');
function read(filename, callback) {
fs.readFile(path.join(__dirname, filename), { encoding: 'utf-8' }, callback);
}
module.exports = {
read,
};
callback.test.js:
const { read } = require('./callback');
const fs = require('fs');
describe('callback testing', () => {
test('callback is called', () => {
const mockCallback = jest.fn();
const mockData = 'mock file data';
const mockReadFile = jest.spyOn(fs, 'readFile').mockImplementationOnce((filename, options, callback) => {
callback(null, mockData);
});
read('./test', mockCallback);
expect(mockCallback).toHaveBeenCalled();
expect(mockReadFile).toBeCalledWith(expect.any(String), { encoding: 'utf-8' }, mockCallback);
mockReadFile.mockRestore();
});
});
unit test result:
PASS examples/65361608/callback.test.js
callback testing
✓ callback is called (3 ms)
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
callback.js | 100 | 100 | 100 | 100 |
-------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.992 s, estimated 5 s
I'm doing testing in node.js for the first time. I've created a simple program below. An html form has two fields, name and age. When a user submits the form, the age is checked in certificate function inside 'ageVerification.js' for it should be more than 18. If age is less than 18, null is returned otherwise some random number. Had the same getAuth function thrown error, then i would test it using
expect(auth.getAuth.bind(this,req,{})).to.throw('whatever thrown')
but if the function is just ended with res.send("some string"), then how to test the function. Thanks in advance.
**code to test**
const certificate=require('./ageVerification')
module.exports.getAuth=function(req,res){
if(certificate.certificate(req.body.ageText)!=null &&
req.body.uName=='prateek'){
res.write("Hello Prateek")
return res.end()
}
if(certificate.certificate(req.body.ageText)!=null &&
req.body.uName!='prateek')
return res.send("You need to register first")
if(certificate.certificate(req.body.ageText)==null){
res.write("You are not of required age")
return res.end()
}
}
**test case**
const certificate=require('../ageVerification')
const auth=require('../controller')
const chai=require('chai')
const expect=require('chai').expect;
describe("Age field is null",function(){
it("name is correct",function(done){
const req={
body:{
ageText:null,
uName:'prateek'
}
}
expect(auth.getAuth.bind(this,req,{})).[I'm stuck here ???]
})
})
You need to use sinonjs to make stubs for res.send, res.write and res.end methods and assert them have been called or not to check the code logic.
E.g.
controller.js:
const certificate = require('./certificate');
module.exports.getAuth = function(req, res) {
if (certificate.certificate(req.body.ageText) != null && req.body.uName == 'prateek') {
res.write('Hello Prateek');
return res.end();
}
if (certificate.certificate(req.body.ageText) != null && req.body.uName != 'prateek')
return res.send('You need to register first');
if (certificate.certificate(req.body.ageText) == null) {
res.write('You are not of required age');
return res.end();
}
};
certificate.js:
exports.certificate = function(age) {
return age;
};
controller.test.js:
const auth = require('./controller');
const sinon = require('sinon');
describe('61943894', function() {
afterEach(() => {
sinon.restore();
});
it('should return required age message', function() {
const mReq = {
body: {
ageText: null,
uName: 'prateek',
},
};
const mRes = {
write: sinon.stub().returnsThis(),
send: sinon.stub().returnsThis(),
end: sinon.stub(),
};
auth.getAuth(mReq, mRes);
sinon.assert.calledOnceWithExactly(mRes.write, 'You are not of required age');
sinon.assert.calledOnce(mRes.end);
});
it('should say hello', () => {
const mReq = {
body: {
ageText: 29,
uName: 'prateek',
},
};
const mRes = {
write: sinon.stub().returnsThis(),
send: sinon.stub().returnsThis(),
end: sinon.stub(),
};
auth.getAuth(mReq, mRes);
sinon.assert.calledOnceWithExactly(mRes.write, 'Hello Prateek');
sinon.assert.calledOnce(mRes.end);
});
it('should return register message', () => {
const mReq = {
body: {
ageText: 29,
uName: '',
},
};
const mRes = {
write: sinon.stub().returnsThis(),
send: sinon.stub().returnsThis(),
end: sinon.stub(),
};
auth.getAuth(mReq, mRes);
sinon.assert.calledOnceWithExactly(mRes.send, 'You need to register first');
});
});
unit test results with coverage report:
61943894
✓ should return required age message
✓ should say hello
✓ should return register message
3 passing (34ms)
----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------|---------|----------|---------|---------|-------------------
All files | 100 | 90 | 100 | 100 |
certificate.js | 100 | 100 | 100 | 100 |
controller.js | 100 | 90 | 100 | 100 | 12
----------------|---------|----------|---------|---------|-------------------
I have the following classes A and B,
class A {
listenResponse(port, callback) {
const server = new CustomServer(port, (error, response) => {
if (error) return callback(error);
callback(null, response);
});
}
}
class CustomServer {
constructor(port, callback) {
this.port = port;
this.server = http.createServer((request, response) => {
if(// condition) { return callback(error); }
callback(null, response);
});
}
}
How do I test the function passed to CustomServer constructor while unit testing class A? Any help is highly appreciated.
Here is the unit test solution using an additional module proxyquire:
a.js:
const CustomServer = require('./customServer');
class A {
listenResponse(port, callback) {
const server = new CustomServer(port, (error, response) => {
if (error) return callback(error);
callback(null, response);
});
}
}
module.exports = A;
customServer.js:
class CustomServer {
constructor(port, callback) {
this.port = port;
this.server = http.createServer((request, response) => {
callback(null, response);
});
}
}
module.exports = CustomServer;
a.test.js:
const sinon = require('sinon');
const proxyquire = require('proxyquire');
describe('60084056', () => {
it('should get response', () => {
const error = null;
const response = {};
const customServerStub = sinon.stub().yields(error, response);
const A = proxyquire('./a.js', {
'./customServer': customServerStub,
});
const a = new A();
const port = 3000;
const callback = sinon.stub();
a.listenResponse(port, callback);
sinon.assert.calledWithExactly(customServerStub, port, sinon.match.func);
sinon.assert.calledWithExactly(callback, null, response);
});
it('should handle error', () => {
const error = new Error('network');
const response = {};
const customServerStub = sinon.stub().yields(error, response);
const A = proxyquire('./a.js', {
'./customServer': customServerStub,
});
const a = new A();
const port = 3000;
const callback = sinon.stub();
a.listenResponse(port, callback);
sinon.assert.calledWithExactly(customServerStub, port, sinon.match.func);
sinon.assert.calledWithExactly(callback, error);
});
});
Unit test results with coverage report:
60084056
✓ should get response (1684ms)
✓ should handle error
2 passing (2s)
-----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------|---------|----------|---------|---------|-------------------
All files | 70 | 100 | 50 | 66.67 |
a.js | 100 | 100 | 100 | 100 |
customServer.js | 25 | 100 | 0 | 25 | 3-5
-----------------|---------|----------|---------|---------|-------------------
Source code: https://github.com/mrdulin/expressjs-research/tree/master/src/stackoverflow/60084056
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