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
------------------|---------|----------|---------|---------|-------------------
Related
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 |
------------|---------|----------|---------|---------|-------------------
I was testing my 'user' code with mocha and chai, But I keep getting this error. The index.js file which creates models is auto build and was working fine. Then I tried adding tests using mocha and couldn't figure out the reason.
my test folders :
test/user/user_dbtest:
const {expect}=require('chai')
const{usernameExists}=require('../../services/user/user_db')
describe('user db tests',()=>{
it('check whether username alreddy exists',async()=>{
const check=await usernameExists('')
expect(check).to.be.false
expect(check===null).to.be.false
expect(check===undefined).to.be.false
});
});
services/user/user_db :
const db=require('../../models/index.js')
async function usernameExists(username){
const user= await db.user.findOne({
where:{username}
});
if(user) return user
return false
}
module.exports={usernameExists}
models/index.js :
'use strict';
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const config = require('../config/config');
const db = {};
console.log(config)
const sequelize=new Sequelize(config.db.database,config.db.username,config.db.password,{
dialect:'mysql',
host:config.db.host
})
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
})
.forEach(file => {
const model = sequelize['import'](path.join(__dirname, file));
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
//ASSOCIATIONS
db.book_belongs_to.belongsTo(db.user)
db.book_belongs_to.belongsTo(db.book)
db.book_bought_by.belongsTo(db.user)
db.book_bought_by.belongsTo(db.book)
sequelize
.authenticate()
.then(()=>{
console.log("connected");
})
.catch(err=>{
console.error(err);
});
module.exports = db;
models/user.js
'use strict';
module.exports=(sequelize,Datatypes)=>{
return sequelize.define('user',{
username:{
type:Datatypes.STRING,
allowNull:false
},
firstname:{
type:Datatypes.STRING,
allowNull:false
},
middlename:{
type:Datatypes.STRING,
},
lastname:{
type:Datatypes.STRING,
allowNull:false
},
email:{
type:Datatypes.STRING,
allowNull:false,
isEmail:true
},
password:{
type:Datatypes.STRING,
allowNull:false
}},{
timestamps:false,
freezeTableName:true
})
}
Thanks in advance...
You should use a stub/mock library such sinon.js to mock db.user.findOne method.
E.g.
user_db.js:
const db = require('./index.js');
async function usernameExists(username) {
const user = await db.user.findOne({
where: { username },
});
if (user) return user;
return false;
}
module.exports = { usernameExists };
index.js:
// simulate sequelize db
const db = {
user: {
findOne() {},
},
};
module.exports = db;
Because we are testing the service layer, the model layer doesn't matter(We stub and mock the models). So, there is no model layer in this example.
user_db.test.js:
const { expect } = require('chai');
const sinon = require('sinon');
const { usernameExists } = require('./user_db');
const db = require('./index.js');
describe('user db tests', () => {
afterEach(() => {
sinon.restore();
});
it('should return non-existed user', async () => {
sinon.stub(db.user, 'findOne').resolves(false);
const check = await usernameExists('');
expect(check).to.be.false;
expect(check === null).to.be.false;
expect(check === undefined).to.be.false;
});
it('should return existed user', async () => {
const fakeUser = { username: 'anna' };
sinon.stub(db.user, 'findOne').resolves(fakeUser);
const check = await usernameExists('');
expect(check).to.be.deep.equal({ username: 'anna' });
});
});
unit test results with coverage report:
user db tests
✓ should return non-existed user
✓ should return existed user
2 passing (27ms)
------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 50 | 100 |
index.js | 100 | 100 | 0 | 100 |
user_db.js | 100 | 100 | 100 | 100 |
------------|---------|----------|---------|---------|-------------------
source code: https://github.com/mrdulin/expressjs-research/tree/master/src/stackoverflow/61695519
//routes.js
app.get('/:id/info',
UnoController.getGameInfo,
...
);
//UnoController.js
async function getGameInfo(req, res) {
data = await UnoModel.getGameInfo(req.params.id);
if(data==null) return res.status(404).json({message:'Room Not Found'});
res.json(data);
}
//UnoModel.js
exports.getGameInfo = async function (id) {
return await mongodb.findById('uno', id);
}
I am writing unit testing in node js using sinon.
I want stub the UnoModel.getGameInfo to return {id:'123456789012'}, When i hit /someid/info rest api.
I wrote the test case like below.
//UnoApiTest.js
it('get game info with player payload and invalid room id', function (done) {
sinon.stub(UnoModel, 'getGameInfo').resolves({ id: '123456789012' });
request({
url: 'http://localhost:8080/api/v1/game/uno/123456789012/info',
headers: { 'x-player-token': jwt.sign({ _id: '123' }) }
}, function (error, response, body) {
expect(response.statusCode).to.equal(200);
done();
});
});
But i am receiving the statusCode as 404.
I tried to console the data. Its actually fetching from db.
It doesn't returns the provided value for stub.
Can anyone help me with this?
Is there any other way to do this?
It should work. E.g.
server.js:
const express = require('express');
const UnoController = require('./UnoController');
const app = express();
app.get('/api/v1/game/uno/:id/info', UnoController.getGameInfo);
module.exports = app;
UnoController.js:
const UnoModel = require('./UnoModel');
async function getGameInfo(req, res) {
const data = await UnoModel.getGameInfo(req.params.id);
if (data == null) return res.status(404).json({ message: 'Room Not Found' });
res.json(data);
}
exports.getGameInfo = getGameInfo;
UnoModel.js:
// simulate mongodb
const mongodb = {
findById(arg1, arg2) {
return { name: 'james' };
},
};
exports.getGameInfo = async function(id) {
return await mongodb.findById('uno', id);
};
UnoApiTest.test.js:
const app = require('./server');
const UnoModel = require('./UnoModel');
const request = require('request');
const sinon = require('sinon');
const { expect } = require('chai');
describe('61172026', () => {
const port = 8080;
let server;
before((done) => {
server = app.listen(port, () => {
console.log(`http server is listening on http://localhost:${port}`);
done();
});
});
after((done) => {
server.close(done);
});
it('should pass', (done) => {
sinon.stub(UnoModel, 'getGameInfo').resolves({ id: '123456789012' });
request({ url: 'http://localhost:8080/api/v1/game/uno/123456789012/info' }, function(error, response, body) {
expect(response.statusCode).to.equal(200);
done();
});
});
});
API automation test results with coverage report:
61172026
http server is listening on http://localhost:8080
✓ should pass
1 passing (50ms)
------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------|---------|----------|---------|---------|-------------------
All files | 80 | 50 | 33.33 | 85.71 |
UnoController.js | 83.33 | 50 | 100 | 100 | 5
UnoModel.js | 50 | 100 | 0 | 50 | 4,8
server.js | 100 | 100 | 100 | 100 |
------------------|---------|----------|---------|---------|-------------------
source code: https://github.com/mrdulin/expressjs-research/tree/master/src/stackoverflow/61172026
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.