I am unit-testing an express function. The express function uses an import which I want to mock (stripe). How can I mock an import while using express function? The issue is that it is a global import, if I could somehow make this an argument of the express function my problem would be solved.
express code (exported to the test file as firebase_functions.subscribe)
const functions = require('firebase-functions');
const stripe = require('stripe')(functions.config().stripe.client_id);
module.exports = async function( request, response ) {
if( !request.query.id_token ) {
response.status(400).json({message: 'id_token has not been provided'});
return;
}
await admin.auth()
.verifyIdToken( request.query.id_token )
.then( id_token => {
// pseudo-code: something is done with e.g.
const message = await stripe.xxx( id_token ); // returns 'some result I got using the stripe Mock in the process'
response.status(200).json({message});
return;
})
.catch( error => {
return response.status(401).json({message: 'You are currently not logged in as an authorised user'});
})
}
excerpt from the test-code
describe('subscribe( request, response)', () => {
it( 'should redirect an user to an url for stripe subscriptions', async() => {
// arrange
const response = new ResponseSpy();
const id_token = await get_id_token();
const request = { query: { id_token }};
// act
await firebase_functions.subscribe( request, response );
// assert
assert.deepStrictEqual( result.json, {message: 'some result I got using the stripe Mock in the process'} );
});
});
Since you are using firebase-functions, you'd better use firebase-functions-test package to test these cloud functions.
In order to mock standalone function stripe, you need proxyquire package. For more info, see How to use Link Seams with CommonJS.
I will use sinon.js as the stub library. And, I will use offline mode of firebase-functions-test package to writing the unit testings.
Here is the unit test solution:
index.js:
const admin = require('firebase-admin');
const app = admin.initializeApp();
const functions = require('firebase-functions');
const stripe = require('stripe')(functions.config().stripe.client_id);
module.exports = async function (request, response) {
if (!request.query.id_token) {
response.status(400).json({ message: 'id_token has not been provided' });
return;
}
await app
.auth()
.verifyIdToken(request.query.id_token)
.then(async (id_token) => {
const message = await stripe.xxx(id_token);
response.status(200).json({ message });
return;
})
.catch((error) => {
return response.status(401).json({ message: 'You are currently not logged in as an authorised user' });
});
};
index.test.js:
const test = require('firebase-functions-test')();
const admin = require('firebase-admin');
test.mockConfig({ stripe: { client_id: 'client_123' } });
const sinon = require('sinon');
const proxyquire = require('proxyquire');
describe('66157457', () => {
afterEach(() => {
sinon.restore();
});
it('should send message', async () => {
const authStub = { auth: sinon.stub().returnsThis(), verifyIdToken: sinon.stub().resolves('token_123') };
const adminInitStub = sinon.stub(admin, 'initializeApp').returns(authStub);
const request = { query: { id_token: 'abc' } };
const response = { status: sinon.stub().returnsThis(), json: sinon.stub() };
const stripeStub = { xxx: sinon.stub().resolves('fake message') };
const stripeFactory = sinon.stub().returns(stripeStub);
const subscribe = proxyquire('./', {
stripe: stripeFactory,
});
await subscribe(request, response);
sinon.assert.calledOnce(adminInitStub);
sinon.assert.calledWithExactly(stripeFactory, 'client_123');
sinon.assert.calledOnce(authStub.auth);
sinon.assert.calledWithExactly(authStub.verifyIdToken, 'abc');
sinon.assert.calledWithExactly(stripeStub.xxx, 'token_123');
sinon.assert.calledWithExactly(response.status, 200);
sinon.assert.calledWithExactly(response.json, { message: 'fake message' });
test.cleanup();
});
});
unit test result:
66157457
✓ should send message (145ms)
1 passing (148ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 76.92 | 50 | 66.67 | 76.92 |
index.js | 76.92 | 50 | 66.67 | 76.92 | 8-9,21
----------|---------|----------|---------|---------|-------------------
Related
I am trying to mock an async function which internally calls n async method in another js file. When i mock the the async function, it says 'expected post to be caaled atleast once'. Can someone please help? Thank you very much in advance.
Controller.js
//all imports done here
var producer= require(./Producer)
const result = async function(req,res,"login"){
const link = req.query.param === team ? '/login' : '/search';
await producer.produce(req,res);
const result = await axios.post(link,req.name,req.dob):
await producer.produce(req,res,"logout");
}
Producer.js
const { EventHubProducerClient } = require("#azure/event-hubs");
const connectionString = "EVENT HUBS NAMESPACE CONNECTION STRING";
const eventHubName = "EVENT HUB NAME";
const produce = async function (req,res,data) =>{
// Create a producer client to send messages to the event hub.
try{
const producer = new EventHubProducerClient(connectionString, eventHubName);
// Prepare a batch of three events.
const batch = await producer.createBatch();
batch.tryAdd({ body: "First event" });
batch.tryAdd({ body: "Second event" });
batch.tryAdd({ body: "Third event" });
// Send the batch to the event hub.
await producer.sendBatch(batch);
// Close the producer client.
await producer.close();
console.log("A batch of three events have been sent to the event hub");
}catch(error){
throw e;
}
}
controller-test.js
const controller = require(./controller);
describe("execute", =>{
sinon.stub().restore();
const req= {name:"tina", dob:"2-12-2000"};
it("call method to post" =>{
const res = controller.result();
//test fails in the below line
sinon.assert(axios.post,"http://dummyurl/login,req);
});
});
You can use sinon.stub(obj, 'method') to stub producer.produce() method and axios.post() method.
Besides, you should use async/await for testing asynchronous code, so that Mocha will know that it should wait for this function to be called to complete the test.
E.g.
Controller.js:
const producer = require('./Producer');
const axios = require('axios');
const result = async function (req, res) {
const team = 'thunder';
const link = req.query.param === team ? '/login' : '/search';
await producer.produce(req, res);
const result = await axios.post(link, req.name, req.dob);
await producer.produce(req, res, 'logout');
};
module.exports = { result };
Producer.js
const { EventHubProducerClient } = require('#azure/event-hubs');
const connectionString = 'EVENT HUBS NAMESPACE CONNECTION STRING';
const eventHubName = 'EVENT HUB NAME';
const produce = async (req, res, data) => {
try {
const producer = new EventHubProducerClient(connectionString, eventHubName);
const batch = await producer.createBatch();
batch.tryAdd({ body: 'First event' });
batch.tryAdd({ body: 'Second event' });
batch.tryAdd({ body: 'Third event' });
await producer.sendBatch(batch);
await producer.close();
console.log('A batch of three events have been sent to the event hub');
} catch (error) {
throw e;
}
};
module.exports = { produce };
Controller.test.js:
const sinon = require('sinon');
const axios = require('axios');
const controller = require('./controller');
const producer = require('./Producer');
describe('execute', () => {
afterEach(() => {
sinon.restore();
});
it('call method to post', async () => {
sinon.stub(axios, 'post');
sinon.stub(producer, 'produce');
const req = { name: 'tina', dob: '2-12-2000', query: { param: 'thunder' } };
await controller.result(req, {});
sinon.assert.calledWithExactly(axios.post, '/login', 'tina', '2-12-2000');
sinon.assert.calledTwice(producer.produce);
});
});
Test result:
execute
✓ call method to post
1 passing (5ms)
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 58.33 | 50 | 50 | 58.33 |
Producer.js | 33.33 | 100 | 0 | 33.33 | 7-17
controller.js | 100 | 50 | 100 | 100 | 6
---------------|---------|----------|---------|---------|-------------------
I am using fastify framework for my node.js application and sequelize as an ORM. I am using mocha, chai, and Sinon for unit testing. I have to unit test my controller function. The following is the sample controller function.
// controllers.js;
const service = require('../services');
exports.create = (req, reply) => {
const attributes = req.body;
service.create(attributes)
.then((result) => {
reply.code(201).send(result);
})
.catch((error) => {
reply.send(error);
});
};
and my services file is as follows,
// services.js;
const { Model } = require('../models');
function create(attributes) {
return Model.create(attributes);
}
module.exports = { create };
In the above code, I want to unit test only the 'create' function in controllers.js. The problem is, it should not call the database, since it is unit testing. But the Model.create in service.js file will make a call to the database. How can I unit test controller function only?
You should stub service.create method and create mocked req, reply objects.
E.g.
controller.js:
const service = require('./service');
exports.create = (req, reply) => {
const attributes = req.body;
service
.create(attributes)
.then((result) => {
reply.code(201).send(result);
})
.catch((error) => {
reply.send(error);
});
};
service.js:
const { Model } = require('./models');
function create(attributes) {
return Model.create(attributes);
}
module.exports = { create };
models.js:
const Model = {
create() {
console.log('real implementation');
},
};
module.exports = { Model };
controller.test.js:
const controller = require('./controller');
const service = require('./service');
const sinon = require('sinon');
const flushPromises = () => new Promise(setImmediate);
describe('62536251', () => {
afterEach(() => {
sinon.restore();
});
it('should create', async () => {
const mResult = 'success';
sinon.stub(service, 'create').resolves(mResult);
const mReq = { body: {} };
const mReply = { code: sinon.stub().returnsThis(), send: sinon.stub() };
controller.create(mReq, mReply);
await flushPromises();
sinon.assert.calledWith(mReply.code, 201);
sinon.assert.calledWith(mReply.send, 'success');
});
it('should handle error', async () => {
const mError = new Error('network');
sinon.stub(service, 'create').rejects(mError);
const mReq = { body: {} };
const mReply = { code: sinon.stub().returnsThis(), send: sinon.stub() };
controller.create(mReq, mReply);
await flushPromises();
sinon.assert.calledWith(mReply.send, mError);
});
});
unit test result with coverage report:
62536251
✓ should create
✓ should handle error
2 passing (13ms)
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 83.33 | 100 | 60 | 83.33 |
controller.js | 100 | 100 | 100 | 100 |
models.js | 66.67 | 100 | 0 | 66.67 | 3
service.js | 66.67 | 100 | 0 | 66.67 | 4
---------------|---------|----------|---------|---------|-------------------
I want to mock specific methods of the "crypto" module of nodejs.
I am testing the end-points in my code using jest framework.
Here is how my end-point code looks like for email-verification:
/* engnr_cntrlr.js */
exports.engineeremail_verifcation = async(req, res) => {
try {
// 3. i want to mock the next statement and return a mock-value to hashedToken variable
const hashedToken = crypto
.createHash('sha256')
.update(req.params.token)
.digest('hex');
const engnr = await Engineer.findOne({
engnrToken : hashedToken
}).orFail(new Error ('engnr not found'));
engnr.emailVerify = true
await engnr.save()
return res.status(202).send({ message: 'email verified' });
} catch (error) {
res.status(400).send({ error: error.message });
}
};
test script :
/* tests/engineer.test.js */
test('engineer verifies his email', async done => {
// 1. i am fetching an engnr using an email id
engnr = await Engineer.findOne({
email: engineerOne.email
});
try {
const response = await request(app)
.put(`/api/engineer/confirm_mail/${engnr.token}`) //2. i am sending a hashed token as a req.params
.send();
expect(response.statusCode).toBe(202);
expect(response.body).toMatchObject({
message: 'email verified'
});
done();
} catch (error) {
done(error);
}
});
The problem i am facing is to mock the crypto implementation in my 'engineeremail_verification' (just below comment no.3). I am using other crypto methods in other parts of my code and I do not want to mock them. I just want to mock this specific implementation of crypto module with a mock value being returned. How do I do that? Thank You for going through my question. Appreciate any sort of help.
You can use jest.spyOn(object, methodName) to mock crypto.createHash separately.
E.g.
app.js:
const crypto = require('crypto');
const express = require('express');
const app = express();
app.put('/api/engineer/confirm_mail/:token', async (req, res) => {
try {
const hashedToken = crypto.createHash('sha256').update(req.params.token).digest('hex');
console.log(hashedToken);
return res.status(202).send({ message: 'email verified' });
} catch (error) {
res.status(400).send({ error: error.message });
}
});
app.put('/example/:token', async (req, res) => {
const hashedToken = crypto.createHash('sha256').update(req.params.token).digest('hex');
console.log(hashedToken);
res.sendStatus(200);
});
module.exports = app;
app.test.js:
const app = require('./app');
const request = require('supertest');
const crypto = require('crypto');
describe('61368162', () => {
it('should verify email', async () => {
const hashMock = {
update: jest.fn().mockReturnThis(),
digest: jest.fn().mockReturnValueOnce('encrypt 123'),
};
const createHashMock = jest.spyOn(crypto, 'createHash').mockImplementationOnce(() => hashMock);
const logSpy = jest.spyOn(console, 'log');
const engnr = { token: '123' };
const response = await request(app).put(`/api/engineer/confirm_mail/${engnr.token}`).send();
expect(createHashMock).toBeCalledWith('sha256');
expect(hashMock.update).toBeCalledWith('123');
expect(hashMock.digest).toBeCalledWith('hex');
expect(logSpy).toBeCalledWith('encrypt 123');
expect(response.statusCode).toBe(202);
expect(response.body).toMatchObject({ message: 'email verified' });
createHashMock.mockRestore();
logSpy.mockRestore();
});
it('should restore crypto methods', async () => {
const logSpy = jest.spyOn(console, 'log');
const response = await request(app).put('/example/123');
expect(jest.isMockFunction(crypto.createHash)).toBeFalsy();
expect(response.statusCode).toBe(200);
expect(logSpy).toBeCalledWith(expect.any(String));
logSpy.mockRestore();
});
});
integration test results with coverage report:
PASS stackoverflow/61368162/app.test.js (13.621s)
61368162
✓ should verify email (44ms)
✓ should restore crypto methods (5ms)
console.log node_modules/jest-environment-enzyme/node_modules/jest-mock/build/index.js:866
encrypt 123
console.log node_modules/jest-environment-enzyme/node_modules/jest-mock/build/index.js:866
a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 93.75 | 100 | 100 | 92.86 |
app.js | 93.75 | 100 | 100 | 92.86 | 12
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 15.707s
source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/61368162
I have a a controller function like below.
SendOTPController.js
const otpService = require('../services/otpService')
module.exports = async function(req, res) {
const {error, data} = await sendOTP(req.query.phone)
if(error)
return res.send(error)
return res.send(data)
}
otpService.js
module.exports = async function(phone) {
await result = fetch(`http://api.send-otp?phone=${phone}`)
if (result !== sucess)
return {
error: "Failed to send OTP!"
data: null
}
return {
error: null
data: result
}
}
Below is my test.
const expect = require('chai').expect
const request = require('supertest')
const sinon = require('sinon')
const rewire = require('rewire')
const SendOTPController= rewire('../../src/controllers/SendOTPController')
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"
})
SendOTPController.__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)
console.log(result.body)
expect(stub.called).to.be.true
})
})
In above code the stub is not being called.
But if use only controller without using express app it works fine.
const expect = require('chai').expect
const request = require('supertest')
const sinon = require('sinon')
const rewire = require('rewire')
const SendOTPController= rewire('../../src/controllers/SendOTPController')
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"
})
SendOTPController.__set__('sendOTPOnPhone', stub)
const result = await SendOTPController() // not using express app, hence not passing req, res
console.log(result)
expect(stub.called).to.be.true
})
})
I went through many modules and docs.
They give a solution how I can stub a module.exports = async function(){}.
They also work, but only If they are directly imported and tested.
They don't work if I use it with express app.
Any help would be appreciated, thanks.
Instead of returns try to use resolves:
let stub = sinon.stub().resolves({
error: null,
data: "OTP sent"
})
returns is for sync code, resolves for async.
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