Stub a standalone module.exports function using rewire - javascript

I am trying to stub a module.exports function. But I have some trouble. I will give you a sudo code of the situation.
MyController.js
const sendOTPOnPhone = rewire('../../src/services/OtpService/sendOTPOnPhone')
module.exports = async function(req, res) {
const { error, data } = await sendOTPOnPhone(req.query.phone) //this is I want to stub
if(error)
return return res.send(error)
return res.send(data)
}
sendOTPService.js
module.exports = async function(phone) {
const result = await fetch(`external-api-call`)
if(result.status !== 'success')
return {
error: "Failed to send OTP!",
data: null
}
return {
error: null,
data: result
}
}
sendOTPTest.js
const expect = require('chai').expect
const request = require('supertest')
const sinon = require('sinon')
const rewire = require('rewire')
const sendOTPOnPhone = rewire('../../src/services/OtpService/sendOTPOnPhone')
const app = require('../../src/app')
describe('GET /api/v1/auth/otp/generate', function () {
it('should generate OTP', async () => {
let stub = sinon.stub().returns({
error: null,
data: "OTP sent"
})
sendOTPOnPhone.__set__('sendOTPOnPhone', stub)
const result = await request(app)
.get('/api/v1/auth/otp/generate?phone=8576863491')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200)
expect(stub.calledOnce).to.be.true
console.log(result.body)
// expect(result).to.equal('promise resolved');
})
})
Above test is failing, stub is not being called. I don't know what am I missing? If I do this in my sendOTPService:
const sendOTP = async function() {}
module.exports = {
sendOTP
}
and this in the controller.
const { error, data } = sendOTPOnPhone.sendOTPOnPhone(req.query.phone)
It works.
But I import it like const {sendOTPOnPhone } = require('../sendOTPService') It doesn't work.
I am aware that destructing changes the reference of the object.
Can someone suggest a workaround?
Is it possible to achieve this using rewire? OR It can be done with proxyquire.Please can someone suggest?

Here is the integration testing solution using proxyquire, you should use Globally override require.
app.js:
const express = require('express');
const controller = require('./controller');
const app = express();
app.get('/api/v1/auth/otp/generate', controller);
module.exports = app;
controller.js:
let sendOTPOnPhone = require('./sendOTPOnPhone');
module.exports = async function(req, res) {
const { error, data } = await sendOTPOnPhone(req.query.phone);
if (error) return res.send(error);
return res.send(data);
};
sendOTPOnPhone.js:
module.exports = async function(phone) {
const result = await fetch(`external-api-call`);
if (result.status !== 'success')
return {
error: 'Failed to send OTP!',
data: null,
};
return {
error: null,
data: result,
};
};
sendOTP.test.js:
const request = require('supertest');
const sinon = require('sinon');
const proxyquire = require('proxyquire');
describe('GET /api/v1/auth/otp/generate', function() {
it('should generate OTP', async () => {
let stub = sinon.stub().resolves({
error: null,
data: { message: 'OTP sent' },
});
stub['#global'] = true;
const app = proxyquire('./app', {
'./sendOTPOnPhone': stub,
});
const result = await request(app)
.get('/api/v1/auth/otp/generate?phone=8576863491')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200);
sinon.assert.calledOnce(stub);
console.log(result.body);
});
});
Integration test results with coverage report:
GET /api/v1/auth/otp/generate
{ message: 'OTP sent' }
✓ should generate OTP (2373ms)
1 passing (2s)
-------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------------|---------|----------|---------|---------|-------------------
All files | 68.75 | 25 | 50 | 73.33 |
app.js | 100 | 100 | 100 | 100 |
controller.js | 83.33 | 50 | 100 | 100 | 5
sendOTPOnPhone.js | 20 | 0 | 0 | 20 | 2-4,8
-------------------|---------|----------|---------|---------|-------------------
source code: https://github.com/mrdulin/expressjs-research/tree/master/src/stackoverflow/60599945

I have answered a similar question here stub SMS otp method (without using external dependecy like proxyquire)
basically, the problem is with exports. Move your sendOtp method to a generalized place e.g User Model or schema. Import model, stub its function. It should work fine.
you are stubbing imported property called sendOtp instead of original function.

Related

Stub of Sinon using axios returns undefined

I'm testing using sinon with axios.
// index.js
{
.. more code
const result = await axios.get("http://save")
const sum = result.data.sum
}
And I made a test code by sinon and supertest for e2e test.
// index.test.js
describe('ADMINS GET API, METHOD: GET', () => {
it('/admins', async () => {
sandbox
.stub(axios, 'get')
.withArgs('http://save')
.resolves({sum: 12});
await supertest(app)
.get('/admins')
.expect(200)
.then(async response => {
expect(response.body.code).toBe(200);
});
});
});
But when I test it, it gives me this result.
// index.js
{
.. more code
const result = await axios.get("http://save")
const sum = result.data.sum
console.log(sum) // undefined
}
I think I resolved response. But it doesnt' give any response.
It just pass axios on supertest.
How can I return correct data in this case?
Thank you for reading it.
The resolved value should be { data: { sum: 12 } }.
E.g.
index.js:
const express = require('express');
const axios = require('axios');
const app = express();
app.get('/admins', async (req, res) => {
const result = await axios.get('http://save');
const sum = result.data.sum;
res.json({ code: 200, sum });
});
module.exports = { app };
index.test.js:
const supertest = require('supertest');
const axios = require('axios');
const sinon = require('sinon');
const { app } = require('./index');
describe('ADMINS GET API, METHOD: GET', () => {
it('/admins', async () => {
const sandbox = sinon.createSandbox();
sandbox
.stub(axios, 'get')
.withArgs('http://save')
.resolves({ data: { sum: 12 } });
await supertest(app)
.get('/admins')
.expect(200)
.then(async (response) => {
sinon.assert.match(response.body.code, 200);
sinon.assert.match(response.body.sum, 12);
});
});
});
test result:
ADMINS GET API, METHOD: GET
✓ /admins
1 passing (22ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------

Sinon unit testing. How to unit test a function that will return a promise that will call another function with callback?

I am new to Sinon, but I have looked around for a while trying to find an answer for this question..
I have a function I need to test, it returns a promise to call another function will callback..
Below is the function that I need to write test case for:
const bookService = require(./bookService);
const getBook = () => {
const bookName = "book";
const bookID = '111';
return new Promise((resolve, reject) => {
bookService.InfoRequest(bookName, bookID, 'GET', res => {
if(res.error){
reject(res);
}else{
const list = res['allPages'] || [];
if(list = []){
resolve({
pageNumber: 0,
note: "book is no longer exist"
});
}else{
resolve(res['allPages']);
}
}
})
})
}
The bookService.InfoRequest method is not returning anything it returns the callback(res);
I have tried stub the bookService.InfoRequest method, but since it is not returning anything I am not sure how can I modified the callback parameter to test all 3 branchs..
I am using Ava, so I tried something like this:
test('getBook Error Block', t=> {
const stub = sinon.stub(bookService, InfoRequest);
stub.callsFake(() => {
return { error: true };
});
return obj.getBook().then(res => {
t.deepEqual(res, []);
}).catch(error => {
console.log(error.error);
t.deepEqual(error.error, true);
})
})
This is the test cases for the first Branch, the reject(res) branch. There are 2 more very similar only with different callFake.
But the problem is I am not able to print the error out and test shows it passed, but if I change true to false, it also pass...
The stubbed implementation by .callFake() is not correct. The bookService.InfoRequest() method accepts a callback parameter, the res is passed to this callback. So you need to provide a stubbed implementation with this callback function and pass your fake error.
E.g.
bookService.js:
function InfoRequest(bookName, bookId, method, cb) {}
module.exports = { InfoRequest };
obj.js:
const bookService = require('./bookService');
const getBook = () => {
const bookName = 'book';
const bookID = '111';
return new Promise((resolve, reject) => {
bookService.InfoRequest(bookName, bookID, 'GET', (res) => {
if (res.error) {
reject(res);
} else {
const list = res['allPages'] || [];
if ((list = [])) {
resolve({
pageNumber: 0,
note: 'book is no longer exist',
});
} else {
resolve(res['allPages']);
}
}
});
});
};
module.exports = { getBook };
obj.test.js:
const obj = require('./obj');
const bookService = require('./bookService');
const sinon = require('sinon');
const test = require('ava');
test('getBook Error Block', (t) => {
const res = { error: new Error('network') };
const stub = sinon.stub(bookService, 'InfoRequest').callsFake((bookName, bookId, method, callback) => {
callback(res);
});
return obj.getBook().catch((res) => {
t.deepEqual(res.error, res.error);
sinon.assert.calledWith(stub, 'book', '111', 'GET', sinon.match.func);
});
});
test result:
> nyc ava --timeout=3000 "/Users/dulin/workspace/github.com/mrdulin/expressjs-research/src/stackoverflow/66702460/obj.test.js"
1 test passed
----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------|---------|----------|---------|---------|-------------------
All files | 71.43 | 16.67 | 75 | 71.43 |
bookService.js | 100 | 100 | 0 | 100 |
obj.js | 69.23 | 16.67 | 100 | 69.23 | 11-18
----------------|---------|----------|---------|---------|-------------------

jest + supertest: how to reset the mocked dependency

I am not able to reset jest mocks of a dependency after it's being used once by supertest. I appreciate any help or hint.
The following is my api test with supertest:
import request from 'supertest';
import router from '../index';
const app = require('express')();
app.use(router);
jest.mock('config', () => ({}));
jest.mock('express-request-proxy', () => data => (req, res, next) =>
res.json(data),
);
beforeEach(() => {
jest.resetAllMocks();
});
describe('GET /', () => {
it('should get all stuff', () =>
request(app)
.get('')
.expect(200)
.expect('Content-Type', /json/)
.then(response => {
expect(response.body).toMatchSnapshot(); // all good here
}));
it('should get all stuff when FOO=bar', async () => {
jest.mock('config', () => ({
FOO: 'bar',
get: key => key,
}));
// >> HERE <<
// config.FOO is still undefined!
// reseting mocks did not work ...
await request(app)
.get('')
.expect(200)
.expect('Content-Type', /json/)
.then(response => {
expect(response.body.query).toHaveProperty('baz'); // test fails ...
});
});
});
express.js api:
const router = require('express').Router({ mergeParams: true });
import config from 'config';
import proxy from 'express-request-proxy';
router.get('', (...args) => {
let query = {};
if (config.FOO === 'bar') {
query.baz = true;
}
return proxy({
url: '/stuff',
query,
})(...args);
});
You can't use jest.mock(moduleName, factory, options) in a function scope, it should be used in the module scope. You should use jest.doMock(moduleName, factory, options) if you want to arrange the mocks in function scope of test cases.
We also need to use jest.resetModules() before executing each test case to
reset the module registry - the cache of all required modules.
It means your ./config module registry will be reset so that it will return different mocked values for each test case when you require it after jest.doMock('./config', () => {...}) statement.
{ virtual: true } option means I don't install the express-request-proxy package so it doesn't exist in my npm_modules directory. If you had already installed it, you can remove this option.
Here is the unit test solution:
index.js:
const router = require('express').Router({ mergeParams: true });
import config from './config';
import proxy from 'express-request-proxy';
router.get('', (...args) => {
console.log(config);
let query = {};
if (config.FOO === 'bar') {
query.baz = true;
}
return proxy({ url: '/stuff', query })(...args);
});
export default router;
config.js:
export default {
FOO: '',
};
index.test.js:
import request from 'supertest';
jest.mock('express-request-proxy', () => (data) => (req, res, next) => res.json(data), { virtual: true });
beforeEach(() => {
jest.resetAllMocks();
jest.resetModules();
});
describe('GET /', () => {
it('should get all stuff', () => {
jest.doMock('./config', () => ({}));
const router = require('./index').default;
const app = require('express')();
app.use(router);
return request(app)
.get('')
.expect(200)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body).toMatchSnapshot();
});
});
it('should get all stuff when FOO=bar', async () => {
jest.doMock('./config', () => ({
default: {
FOO: 'bar',
get: (key) => key,
},
__esModule: true,
}));
const router = require('./index').default;
const app = require('express')();
app.use(router);
await request(app)
.get('')
.expect(200)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body.query).toHaveProperty('baz');
});
});
});
unit test results with 100% coverage report:
PASS stackoverflow/61828748/index.test.js (11.966s)
GET /
✓ should get all stuff (8710ms)
✓ should get all stuff when FOO=bar (24ms)
console.log
{}
at stackoverflow/61828748/index.js:7:11
console.log
{ FOO: 'bar', get: [Function: get] }
at stackoverflow/61828748/index.js:7:11
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 1 passed, 1 total
Time: 13.268s
index.test.js.snap:
// Jest Snapshot v1
exports[`GET / should get all stuff 1`] = `
Object {
"query": Object {},
"url": "/stuff",
}
`;
source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/61828748

TypeError: sns.publish is not a function , jest mock AWS SNS

I am trying to mock AWS.SNS and I am getting error. I referred posts on StackOverflow and could come up with below. Still, I am getting an error. I have omitted the irrelevant portion. Can someone please help me?
Below is my index.ts
import { SNS } from "aws-sdk";
export const thishandler = async (event: thisSNSEvent): Promise<any> => {
// I have omitted other code that works and not related to issue I am facing.
// I am receiving correct value of 'snsMessagetoBeSent' I verified that.
const response = await sendThisToSNS(snsMessagetoBeSent);
} // thishandler ends here
async function sendThisToSNS(thisMessage: snsAWSMessage) {
const sns = new SNS();
const TOPIC_ARN = process.env.THIS_TOPIC_ARN;
var params = {
Message: JSON.stringify(thisMessage), /* required */
TopicArn: TOPIC_ARN
};
return await sns.publish(params).promise();
}
My test case is below
jest.mock('aws-sdk', () => {
const mockedSNS = {
publish: jest.fn().mockReturnThis(),
promise: jest.fn()
};
return {
SNS: jest.fn(() => mockedSNS),
};
});
import aws, { SNS } from 'aws-sdk';
const snsPublishPromise = new aws.SNS().publish().promise;
import { thishandler } from "../src/index";
describe("async testing", () => {
beforeEach(() => {
jest.restoreAllMocks();
jest.resetAllMocks();
});
it("async test", async () => {
const ENRICHER_SNS_TOPIC_ARN = process.env.ENRICHER_SNS_TOPIC_ARN;
process.env.ENRICHER_SNS_TOPIC_ARN = "OUR-SNS-TOPIC";
const mockedResponseData ={
"Success": "OK"
};
(snsPublishPromise as any).mockResolvedValueOnce(mockedResponseData);
const result = await thishandler(thisSNSEvent);
});
I get error as TypeError: sns.publish is not a function
Here is the unit test solution:
index.ts:
import { SNS } from 'aws-sdk';
export const thishandler = async (event): Promise<any> => {
const snsMessagetoBeSent = {};
const response = await sendThisToSNS(snsMessagetoBeSent);
return response;
};
async function sendThisToSNS(thisMessage) {
const sns = new SNS();
const TOPIC_ARN = process.env.THIS_TOPIC_ARN;
const params = {
Message: JSON.stringify(thisMessage),
TopicArn: TOPIC_ARN,
};
return await sns.publish(params).promise();
}
index.test.ts:
import { thishandler } from './';
import { SNS } from 'aws-sdk';
jest.mock('aws-sdk', () => {
const mSNS = {
publish: jest.fn().mockReturnThis(),
promise: jest.fn(),
};
return { SNS: jest.fn(() => mSNS) };
});
describe('59810802', () => {
let sns;
beforeEach(() => {
sns = new SNS();
});
afterEach(() => {
jest.clearAllMocks();
});
it('should pass', async () => {
const THIS_TOPIC_ARN = process.env.THIS_TOPIC_ARN;
process.env.THIS_TOPIC_ARN = 'OUR-SNS-TOPIC';
const mockedResponseData = {
Success: 'OK',
};
sns.publish().promise.mockResolvedValueOnce(mockedResponseData);
const mEvent = {};
const actual = await thishandler(mEvent);
expect(actual).toEqual(mockedResponseData);
expect(sns.publish).toBeCalledWith({ Message: JSON.stringify({}), TopicArn: 'OUR-SNS-TOPIC' });
expect(sns.publish().promise).toBeCalledTimes(1);
process.env.THIS_TOPIC_ARN = THIS_TOPIC_ARN;
});
});
Unit test results with 100% coverage:
PASS src/stackoverflow/59810802/index.test.ts (13.435s)
59810802
✓ should pass (9ms)
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
index.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 15.446s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/59810802

Not able to call the res.send()

I am working on a Node.js application.
This is a code of validate.js file, where I have defined a class Validate and static method validateTicket and at the bottom exported the class.
validate.js file
const request = require("request");
const config = { urlBase: "" };
class Validate {
static validateTicket = (ticket) => {
const options = { url: config.urlBase + ticket, json: true, rejectUnauthorized: false };
return new Promise((resolve, reject) => {
request.get(options, (error, response, body) => {
if (error) {
reject(error);
return;
}
if (response) {
resolve(response);
return;
}
reject(null);
});
});
};
}
module.exports = Validate;
This is the main utility file where I am importing the validate.js file and there are 3 functions validateTicketReqs, to_json, and sendErrorResponse.
validateTicketReqs function do validation on response received from class method.
util.js file
const validate = require('./validate.js');
function validateTicketReqs(req, res, next, flag, num) {
let payload = req.body;
let ticket= (payload.ticket) ? payload.ticket: null;
let error_msg = 'Failed to obtain a successful response '
validate.validateTicket(ticket).then(
(resolution) => {
let json = to_json(resolution); // function will return null value for json
if (json === null || !('body' in json)) {
return sendErrorResponse(req, res, 500, 'error');
}
...
})
function sendErrorResponse(req, res, code, msg) {
let resBody = {
msg: msg
};
res.status(code);
res.send(resBody);
}
test_util.js file
const validate = require('./validate.js');
describe("Check", () => {
it("should fail due to error", (done) => {
let req = {
authData: {'user_id': "mock"},
body: {"num": "mock", "tikcet": "mock"},
request_id: 'mock'
};
let res = {send: sinon.spy(), status: sinon.spy()};
let next = sinon.spy();
mock_buffer = {
toJSON : function() { return {body: {environment: 'mock', status: 'mock'}}},
statusCode: 500
}
const validateTicket= sinon.stub(validate, 'validateTicket')
validateTicket.resolves(mock_buffer)
util.valdateTicketReqs(req, res, next, flag, "mock" );
expect(res.send.calledOnce).to.be.true; ##not getting called
expect(res.send.firstCall.args[0].statusCode).to.equal(500); ##not getting called
util.valdateTicketReqs.restore();
validate.validateTicket.restore();
done();
});
});
The problem I am facing is that (res.send.calledOnce) value is coming as false. I am expecting that res.send() should be called at least once but it is not. Please advise what is the problem here. Let me know if you have a query on the question.
The issue is the expect method is called before the test case finished. The return value of your validate.validateTicket method is a promise, it's asynchronous method. You need return it so that test runner will know if the test case is done.
E.g.
validate.ts:
const request = require("request");
const config = { urlBase: "" };
class Validate {
static validateTicket = (ticket) => {
const options = { url: config.urlBase + ticket, json: true, rejectUnauthorized: false };
return new Promise((resolve, reject) => {
request.get(options, (error, response, body) => {
if (error) {
reject(error);
return;
}
if (response) {
resolve(response);
return;
}
reject(null);
});
});
};
}
module.exports = Validate;
util.ts:
const validate = require("./validate");
function to_json(data) {
return null;
}
function sendErrorResponse(req, res, code, msg) {
const resBody = {
msg: msg,
};
res.status(code);
res.send(resBody);
}
export function validateTicketReqs(req, res, next, flag, num) {
const payload = req.body;
const ticket = payload.ticket ? payload.ticket : null;
const error_msg = "Failed to obtain a successful response ";
return validate.validateTicket(ticket).then((resolution) => {
const json = to_json(resolution);
if (json === null || !("body" in json)) {
return sendErrorResponse(req, res, 500, "error");
}
});
}
util.spec.ts:
import sinon from "sinon";
import { expect } from "chai";
import * as util from "./util";
const validate = require("./validate");
describe("Check", () => {
it("should fail due to error", async () => {
const req = {
authData: { user_id: "mock" },
body: { num: "mock", tikcet: "mock" },
request_id: "mock",
};
const res = {
send: sinon.spy(),
status: sinon.spy(),
};
const next = sinon.spy();
const flag = "";
const mock_buffer = {
toJSON: function() {
return { body: { environment: "mock", status: "mock" } };
},
statusCode: 500,
};
const validateTicketStub = sinon.stub(validate, "validateTicket").resolves(mock_buffer);
await util.validateTicketReqs(req, res, next, flag, "mock");
expect(res.send.calledOnce).to.be.true;
expect(res.status.firstCall.args[0]).to.equal(500);
sinon.assert.calledOnce(validateTicketStub);
validateTicketStub.restore();
});
});
Unit test result with coverage report:
Check
✓ should fail due to error
1 passing (19ms)
--------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
--------------|----------|----------|----------|----------|-------------------|
All files | 76.09 | 30 | 63.64 | 75.56 | |
util.spec.ts | 94.12 | 100 | 66.67 | 93.75 | 22 |
util.ts | 100 | 50 | 100 | 100 | 9,14 |
validate.ts | 37.5 | 0 | 25 | 37.5 |... 12,15,16,17,20 |
--------------|----------|----------|----------|----------|-------------------|
Source code: https://github.com/mrdulin/mocha-chai-sinon-codelab/tree/master/src/stackoverflow/59037819

Categories

Resources