Issues using Sinon to stub out Pino - javascript

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 |
------------|---------|----------|---------|---------|-------------------

Related

Sinon stub out module's function

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
------------------|---------|----------|---------|---------|-------------------

Mocking Segment Analytics with Jest in TS

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
}
};
}
}
};
});

mock a function in nodejs using mocha

I'm writing unit test cases for my node application using mocha on very first time. I have no clear idea about how to mock a function in mocha.
I added the function of my node application. How can a unit test for below mentioned function look like?.
var authHandler = require('./authenticationHandler');
exports.postloginValues = (req, res) => {
var realEdUsername_Update = req.body.uName;
var encodedPassword = Buffer.from(req.body.password).toString('base64');
var jsonData = {
loginName : realEdUsername_Update,
userPassword : encodedPassword
};
var parseData = JSON.stringify(jsonData);
var result = authHandler.validateRealEdUser(parseData, res);
};
index.js:
var authHandler = require('./authenticationHandler');
exports.postloginValues = (req, res) => {
var realEdUsername_Update = req.body.uName;
var encodedPassword = Buffer.from(req.body.password).toString('base64');
var jsonData = {
loginName: realEdUsername_Update,
userPassword: encodedPassword,
};
var parseData = JSON.stringify(jsonData);
var result = authHandler.validateRealEdUser(parseData, res);
};
authenticationHandler.js:
module.exports = {
validateRealEdUser(parseData, res) {},
};
index.test.js:
const { postloginValues } = require('./');
var authHandler = require('./authenticationHandler');
const sinon = require('sinon');
describe('60734436', () => {
it('should pass', () => {
const validateRealEdUserStub = sinon.stub(authHandler, 'validateRealEdUser');
const mReq = { body: { password: '123', uName: 'james' } };
const mRes = {};
postloginValues(mReq, mRes);
sinon.assert.calledWithExactly(
validateRealEdUserStub,
JSON.stringify({ loginName: 'james', userPassword: Buffer.from(mReq.body.password).toString('base64') }),
mRes,
);
validateRealEdUserStub.reset();
});
});
unit test results with coverage report:
60734436
✓ should pass
1 passing (8ms)
--------------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 50 | 100 |
authenticationHandler.js | 100 | 100 | 0 | 100 |
index.js | 100 | 100 | 100 | 100 |
--------------------------|---------|----------|---------|---------|-------------------

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.

Jest mock function's inner variable and change function's behavior?

i have little experience with jest as i am trying to change variable in my function as we expect to have error thrown if the function's inner variable(baseUrl) changed to null, my function :
buildUrl.js:
export function buildUrl(contentId, data, options = {}) {
let baseUrl = config.has('imgBaseUrl') && config.get('imgBaseUrl');
if (!baseUrl) {
throw new Error('some error');
}
}
i need to mock the baseUrl to value of null for example and test it
buildUrl.test.js
import {buildUrl} from "./buildURL";
....
it('throw an error when "baseUrl" is not configured', () => {
let mockBaseUrl = {baseUrl: null};
jest.mock('./buildURL', ()=> mockBaseUrl);
// jest.mock('../../../config/test', ()=>mockImgBaseUrl); // or mock the config to be used by the function?
expect(()=> buildUrl('1.44444', data[0], defaultOptions)).toThrow(
'some error'
);
});
another approach using jest.fn() didnt work as expected , maybe i am missing something here...
Here is the unit test solution:
baseUrl.js:
import config from './config';
export function buildUrl(contentId, data, options = {}) {
let baseUrl = config.has('imgBaseUrl') && config.get('imgBaseUrl');
if (!baseUrl) {
throw new Error('some error');
}
console.log(baseUrl);
}
config.js:
export default {
config: {},
has(key) {
return !!config[key];
},
get(key) {
return config[key];
}
};
baseUrl.spec.js:
import { buildUrl } from './buildUrl';
import config from './config';
describe('buildUrl', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('should throw error when baseUrl is null', () => {
const defaultOptions = {};
const data = ['fake data'];
const hasSpy = jest.spyOn(config, 'has').mockReturnValueOnce(true);
const getSpy = jest.spyOn(config, 'get').mockReturnValueOnce(null);
expect(() => buildUrl('1.44444', data[0], defaultOptions)).toThrowError('some error');
expect(hasSpy).toBeCalledWith('imgBaseUrl');
expect(getSpy).toBeCalledWith('imgBaseUrl');
});
it('should log baseUrl', () => {
const defaultOptions = {};
const data = ['fake data'];
const hasSpy = jest.spyOn(config, 'has').mockReturnValueOnce(true);
const getSpy = jest.spyOn(config, 'get').mockReturnValueOnce('https://github.com/mrdulin');
const logSpy = jest.spyOn(console, 'log');
buildUrl('1.44444', data[0], defaultOptions);
expect(hasSpy).toBeCalledWith('imgBaseUrl');
expect(getSpy).toBeCalledWith('imgBaseUrl');
expect(logSpy).toBeCalledWith('https://github.com/mrdulin');
});
});
Unit test result:
PASS src/stackoverflow/48006588/buildUrl.spec.js (8.595s)
buildUrl
✓ should throw error when baseUrl is null (9ms)
✓ should log baseUrl (7ms)
console.log node_modules/jest-mock/build/index.js:860
https://github.com/mrdulin
-------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-------------|----------|----------|----------|----------|-------------------|
All files | 80 | 83.33 | 33.33 | 77.78 | |
buildUrl.js | 100 | 83.33 | 100 | 100 | 3 |
config.js | 33.33 | 100 | 0 | 33.33 | 4,7 |
-------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 9.768s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/48006588
I think you can do this:
jest.mock('./buildURL', () => {buildUrl: jest.fn()})
buildUrl.mockImplementation(() => {baseUrl: null})
See Jest docs

Categories