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.
Related
Imagine I have a file here:
// a.js
const dbConnection = require('./db-connection.js')
module.exports = function (...args) {
return async function (req, res, next) {
// somethin' somethin' ...
const dbClient = dbConnection.db
const docs = await dbClient.collection('test').find()
if (!docs) {
return next(Boom.forbidden())
}
}
}
, the db-connection.js looks like this:
const MongoClient = require('mongodb').MongoClient
const dbName = 'test'
const url = process.env.MONGO_URL
const client = new MongoClient(url, { useNewUrlParser: true,
useUnifiedTopology: true,
bufferMaxEntries: 0 // dont buffer querys when not connected
})
const init = () => {
return client.connect().then(() => {
logger.info(`mongdb db:${dbName} connected`)
const db = client.db(dbName)
})
}
/**
* #type {Connection}
*/
module.exports = {
init,
client,
get db () {
return client.db(dbName)
}
}
and I have a test to stub out find() method to return at least true (in order to test out if the a.js's return value is true. How can I do that?
The unit test strategy is to stub the DB connection, we don't need to connect the real mongo DB server. So that we can run test cases in an environment isolated from the external environment.
You can use stub.get(getterFn) replaces a new getter for dbConnection.db getter.
Since the MongoDB client initialization happens when you require the db-connection module. You should provide a correct URL for MongoClient from the environment variable before requiring a and db-connection modules. Otherwise, the MongoDB nodejs driver will throw an invalid url error.
E.g.
a.js:
const dbConnection = require('./db-connection.js');
module.exports = function () {
const dbClient = dbConnection.db;
const docs = dbClient.collection('test').find();
if (!docs) {
return true;
}
};
db-connection.js:
const MongoClient = require('mongodb').MongoClient;
const dbName = 'test';
const url = process.env.MONGO_URL;
const client = new MongoClient(url, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const init = () => {
return client.connect().then(() => {
console.info(`mongdb db:${dbName} connected`);
const db = client.db(dbName);
});
};
module.exports = {
init,
client,
get db() {
return client.db(dbName);
},
};
a.test.js:
const sinon = require('sinon');
describe('a', () => {
afterEach(() => {
sinon.restore();
});
it('should find some docs', () => {
process.env.MONGO_URL = 'mongodb://localhost:27017';
const a = require('./a');
const dbConnection = require('./db-connection.js');
const dbStub = {
collection: sinon.stub().returnsThis(),
find: sinon.stub(),
};
sinon.stub(dbConnection, 'db').get(() => dbStub);
const actual = a();
sinon.assert.match(actual, true);
sinon.assert.calledWithExactly(dbStub.collection, 'test');
sinon.assert.calledOnce(dbStub.find);
});
});
Test result:
a
✓ should find some docs (776ms)
1 passing (779ms)
------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------|---------|----------|---------|---------|-------------------
All files | 75 | 50 | 25 | 75 |
a.js | 100 | 50 | 100 | 100 | 7
db-connection.js | 60 | 100 | 0 | 60 | 11-13,21
------------------|---------|----------|---------|---------|-------------------
Based on this question, I need to also make a test for a middleware which also uses the db-connection.js file. The middleware file will look like this:
const dbConnection = require('./db-connection.js')
module.exports = function (...args) {
return async function (req, res, next) {
// somethin' somethin' ...
const dbClient = dbConnection.db
const docs = await dbClient.collection('test').find()
if (!docs) {
return next(Boom.forbidden())
}
}
}
, the database connection file do not change, which is:
const MongoClient = require('mongodb').MongoClient
const dbName = 'test'
const url = process.env.MONGO_URL
const client = new MongoClient(url, { useNewUrlParser: true,
useUnifiedTopology: true,
bufferMaxEntries: 0 // dont buffer querys when not connected
})
const init = () => {
return client.connect().then(() => {
logger.info(`mongdb db:${dbName} connected`)
const db = client.db(dbName)
})
}
/**
* #type {Connection}
*/
module.exports = {
init,
client,
get db () {
return client.db(dbName)
}
}
How the middleware works is by passing list of strings (that strings is roles), I have to query to the database and check whether there is a record of each roles. If the record exists, I will return next(), while if the record does not exist, I will return next(Boom.forbidden()) (next function with a 403 status code from Boom module).
Given the details above, how does one make a test to test out the return value of the middleware if the record exists or not? This means I have to assert the next() and next(Boom.forbidden) to be exact.
Based on the answer. You can create stubs for the req, res objects, and next function.
E.g.(Doesn't run, but it should work.)
const sinon = require('sinon');
describe('a', () => {
afterEach(() => {
sinon.restore();
});
it('should find some docs', async () => {
process.env.MONGO_URL = 'mongodb://localhost:27017';
const a = require('./a');
const dbConnection = require('./db-connection.js');
const dbStub = {
collection: sinon.stub().returnsThis(),
find: sinon.stub(),
};
sinon.stub(dbConnection, 'db').get(() => dbStub);
const req = {};
const res = {};
const next = sinon.stub();
const actual = await a()(req, res, next);
sinon.assert.match(actual, true);
sinon.assert.calledWithExactly(dbStub.collection, 'test');
sinon.assert.calledOnce(dbStub.find);
sinon.assert.calledOnce(next);
});
});
I'm trying to write a unit test that checks if the .track method of the Analytics is called. For some reason the test keeps failing, although calling the function via http does trigger the call. I'm not sure if I've mocked it wrong, or what the issue could be?
index.ts:
import { Request } from "../types"
import { getSecret } from "../src/secrets"
import Analytics from "analytics-node"
const logger = (req: Request) => {
const analytics = new Analytics(<string>process.env.WRITE_KEY);
return analytics.track({
userId: req.userId
});
}
export default logger
index.test.ts:
jest.mock('analytics-node');
import { Request } from "../types"
import logger from "./index"
import Analytics from "analytics-node"
const mockAnalytics = new Analytics(process.env.WRITE_KEY = 'test');
describe('Logger tests', () => {
it(`Should call analytics.track`, () => {
const request: Request = {
userId: 23
}
return logger(request).then(() => {
expect(mockAnalytics.track).toHaveBeenCalled()
});
});
});
You are using Automatic mock by calling jest.mock('analytics-node').
Calling jest.mock('analytics-node') returns a useful "automatic mock" you can use to spy on calls to the class constructor and all of its methods. It replaces the ES6 class with a mock constructor and replaces all of its methods with mock functions that always return undefined. Method calls are saved in theAutomaticMock.mock.instances[index].methodName.mock.calls.
E.g.
index.ts:
import Analytics from 'analytics-node';
export interface Request {
userId: string | number;
}
const logger = (req: Request) => {
const analytics = new Analytics(<string>process.env.WRITE_KEY);
return analytics.track({
userId: req.userId,
anonymousId: 1,
event: '',
});
};
export default logger;
index.test.ts:
import logger, { Request } from './';
import Analytics from 'analytics-node';
jest.mock('analytics-node');
const mockAnalytics = Analytics as jest.MockedClass<typeof Analytics>;
describe('Logger tests', () => {
afterAll(() => {
jest.resetAllMocks();
});
it(`Should call analytics.track`, () => {
const WRITE_KEY = process.env.WRITE_KEY;
process.env.WRITE_KEY = 'test key';
const request: Request = {
userId: 23,
};
logger(request);
expect(mockAnalytics).toBeCalledWith('test key');
expect(mockAnalytics.mock.instances[0].track).toHaveBeenCalled();
process.env.WRITE_KEY = WRITE_KEY;
});
});
unit test result:
PASS examples/65412302/index.test.ts
Logger tests
✓ Should call analytics.track (4 ms)
----------|---------|----------|---------|---------|-------------------
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: 5.954 s
By using "#segment/analytics-next": "1.49.2"
let analyticsInstance: AnalyticsBrowser | null | undefined;
export const loadAnalytics = (props?: LoadAnalyticsProps): typeof analyticsInstance => {
if ((analyticsInstance && analyticsInstance.instance?.initialized) || !props) {
return analyticsInstance;
}
const { segmentAnalyticsKey, anonymousId, userId } = props;
if (!segmentAnalyticsKey) {
return null;
}
analyticsInstance = AnalyticsBrowser.load({ writeKey: segmentAnalyticsKey });
analyticsInstance.setAnonymousId(anonymousId);
analyticsInstance.identify(userId);
return analyticsInstance;
};
mock
const setAnonymousIdMock = jest.fn();
const identifyMock = jest.fn();
jest.mock("#segment/analytics-next", () => {
const originalModule = jest.requireActual("#segment/analytics-next");
return {
__esModule: true,
...originalModule,
AnalyticsBrowser: {
load: () => {
return {
setAnonymousId: setAnonymousIdMock,
identify: identifyMock,
instance: {
initialized: true
}
};
}
}
};
});
I am attempting to write unit test for a service function that returns a Pino log message when ran. I have built a stub, using Sinon, from the logger module I am using in service.js, but have not been able to get a successful test. The test returns: AssertionError: expected error to have been called exactly once, but it was called 0 times
Ideally I would like to be able to assert that the logger was called, and that function returned with the exact logger call and message.
Below is some sample code for what I am trying to achieve
logger.js
const pino = require('pino');
const defaultLogger = pino({}, 'output.log');
module.exports = {
defaultLogger
}
service.js
const path = require('path');
const { defaultLogger } = require('../common/logger');
const logger = defaultLogger.child({ filename: path.basename(__filename) });
const logResponse = () => {
return logger.info('successful');
};
service.test.js
const sinon = require('sinon');
const chai = require('chai');
const sinonChai = require('sinon-chai');
const service = require('../service.js')
const { defaultLogger } = require('../common/logger');
const { expect } = chai;
chai.should();
chai.use(sinonChai);
describe('Service Test', () => {
it('should return a log message', () => {
const spy = sinon.spy(service, 'logResponse')
const stub = sinon.stub(defaultLogger, 'error');
spy.should.have.been.calledOnce;
expect(stub).to.have.been.calledOnce;
spy.should.have.returned(logger.info('successful'));
})
})
Here is the unit test solution:
./logger.js:
const pino = require('pino');
const defaultLogger = pino({}, 'output.log');
module.exports = {
defaultLogger,
};
./service.js:
const path = require('path');
const { defaultLogger } = require('./logger');
const logger = defaultLogger.child({ filename: path.basename(__filename) });
const logResponse = () => {
return logger.info('successful');
};
module.exports = {
logResponse,
};
./service.test.js:
const { defaultLogger } = require('./logger');
const sinon = require('sinon');
const chai = require('chai');
const { expect } = chai;
describe('63618186', () => {
it('should return a log message ', () => {
const loggerStub = {
info: sinon.stub().returns('anything'),
};
sinon.stub(defaultLogger, 'child').returns(loggerStub);
const service = require('./service');
const actual = service.logResponse();
expect(actual).to.be.equal('anything');
sinon.assert.calledOnce(defaultLogger.child);
sinon.assert.calledWithExactly(loggerStub.info, 'successful');
});
});
unit test result with coverage report:
63618186
✓ should return a log message
1 passing (41ms)
------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
logger.js | 100 | 100 | 100 | 100 |
service.js | 100 | 100 | 100 | 100 |
------------|---------|----------|---------|---------|-------------------
Currently, I am trying to write a test. I want to use a local variable in the test, but unfortunately, I don't manage to mock or use it. The variable and the function which uses it, aren't exported.
How do I access the local variable authToken in utils.test.js? So I can change authToken to another value.
I've tried to use rewire (https://www.npmjs.com/package/rewire), but this didn't work. authToken is still undefined.
utils.js
const { auth } = require('./auth.js');
let authToken = undefined;
const checkIfTokenIsValid = async () => {
if (authToken) {
authToken = await auth();
}
};
module.exports = {
// some other functions
}
utils.test.js
const _rewire = require('rewire');
const utils = _rewire('../../lib/resources/utils');
utils.__set__('authToken', () => true);
describe('api auth', () => {
// some tests
});
Here is the unit test solution using rewire module.
utils.js:
let { auth } = require('./auth');
let authToken = undefined;
const checkIfTokenIsValid = async () => {
if (authToken) {
authToken = await auth();
}
};
module.exports = {
checkIfTokenIsValid,
};
auth.js:
async function auth() {
return 'real auth response';
}
module.exports = { auth };
utils.spec.js:
const rewire = require('rewire');
const utils = rewire('./utils');
describe('utils', () => {
describe('#checkIfTokenIsValid', () => {
test('should not check token', async () => {
const authMock = jest.fn();
utils.__set__({
auth: authMock,
authToken: undefined,
});
await utils.checkIfTokenIsValid();
expect(authMock).not.toBeCalled();
});
test('should check token', async () => {
const authMock = jest.fn().mockResolvedValueOnce('abc');
utils.__set__({
auth: authMock,
authToken: 123,
});
await utils.checkIfTokenIsValid();
expect(authMock).toBeCalledTimes(1);
expect(utils.__get__('authToken')).toBe('abc');
});
});
});
Unit test results:
PASS src/stackoverflow/57593767-todo/utils.spec.js
utils
#checkIfTokenIsValid
✓ should not check token (7ms)
✓ should check token (2ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 4.468s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/57593767