Sinon/sandbox test says that function was never called - node.js

I want to write a unit test that checks to see if a function was called, but i'm getting the error:
submitDetails
submitDetails
sendEmail:
AssertionError: expected sendEmail to have been called exactly once, but it was called 0 times
From what I can see my function submitDetails.submitDetails clearly runs the function sendEmail.sendEmail but it's saying that it's never called. I've also tried just using 'spy.called' instead of calledOnce but I get the same result.
Test file:
const submitDetails = require('../src/scripts/submitDetails')
const sendEmail = require('../src/lib/sendEmail')
describe('submitDetails', function () {
let sandbox = null
before(() => {
sandbox = sinon.createSandbox()
})
afterEach(() => {
sandbox.restore()
})
describe('submitDetails', () => {
let mockParams, result
beforeEach(async () => {
sandbox.spy(sendEmail, 'sendEmail')
})
it('sendEmail', () => {
expect(sendEmail.sendEmail).to.have.been.calledOnce()
})
})
})
SubmitDetails.js (file that's being test)
const { sendEmail } = require('../lib/sendEmail')
const submitDetails = {}
submitDetails.submitDetails = query => {
return sendEmail(query)
}
module.exports = submitDetails

You didn't call submitDetails.submitDetails() method in your test case. Here is the working example:
sendEmail.ts:
module.exports = {
sendEmail() {}
};
submitDetails.ts:
const sendEmail = require('./sendEmail');
// #ts-ignore
const submitDetails = {};
// #ts-ignore
submitDetails.submitDetails = query => {
return sendEmail.sendEmail(query);
};
module.exports = submitDetails;
submitDetails.spec.ts:
import { expect } from 'chai';
import sinon, { SinonSandbox, SinonSpy } from 'sinon';
const submitDetails = require('./submitDetails');
const sendEmail = require('./sendEmail');
describe('submitDetails', () => {
let sandbox: SinonSandbox;
before(() => {
sandbox = sinon.createSandbox();
});
afterEach(() => {
sandbox.restore();
});
describe('submitDetails', () => {
let sendEmailSpy: SinonSpy;
beforeEach(() => {
sendEmailSpy = sandbox.spy(sendEmail, 'sendEmail');
});
it('sendEmail', () => {
submitDetails.submitDetails();
sandbox.assert.calledOnce(sendEmailSpy);
expect(sendEmailSpy.calledOnce).to.be.true;
});
});
});
Unit test result:
submitDetails
submitDetails
✓ sendEmail
1 passing (22ms)
Source code: https://github.com/mrdulin/mocha-chai-sinon-codelab/tree/master/src/stackoverflow/58058653

Related

How to write test jest of async/await is toHaveBeenCalled?

`Trying to run a test for the following code using Jest.I mocked the function and checked if it runs with the desired intent. I think the error is in the promise but I can't find a solution. Can you help me?
Why redis.set not call
i get error
expect(jest.fn()).toHaveBeenCalled()
Expected number of calls: >= 1
Received number of calls: 0
mailparser.js
mail parser will call with mgs(‘body’,(stream)=>{parser})
const parser = async (stream) => {
try {
const mail = await simpleParser(stream)
const isAvailable = await redis.set(
[`mail_message_id_${mail.messageId}`],
[`${mail.messageId}`],
{
flag: 'NX',
expiry: 60 * 30,
}
)
if (!isAvailable) {
return console.log(`${mail.messageId} is already processed.`)
}
} catch (err) {
console.error(err, 'failed to parser mail')
}
}
Here my unit Ttesting
const stream = 'stream'
const mockParser = {
simpleParser: jest.fn(),
}
const mockRedis = {
set: jest.fn(),
}
beforeAll(() => {
jest.mock('./redis', () => {
return {
set: mockRedis.set,
}
})
jest.mock('mailparser', () => {
return {
simpleParser: mockParser.simpleParser,
}
})
jest.fn().mockImplementation(() => mockMgs)
})
beforeEach(() => {
mockMgs.on.mockImplementationOnce((event, fn) => {
fn(stream)
})
})
afterEach(async () => {
await jest.clearAllMocks()
})
describe('mailparser', () => {
test('should call redis.set with params correctly', async () => {
const mailparser = require('./mailparser')
await mockParser.simpleParser.mockImplementation(() =>
Promise.resolve(mail) //mail is object data
)
mockRedis.set.mockImplementationOnce(() => Promise.resolve('OK'))
mailparser(mockMgs, 1)
expect(mockRedis.set).toHaveBeenCalled()
})

sinon stub for lambda function which is inside another lambda

Am writing unit test case for my code, as am calling another lambda function inside my lambda am not sure how to mock the inner lambda value, so because of this my test case is getting timed out. Attaching my code below
Test case file
"use strict";
const sinon = require("sinon");
const AWS = require("aws-sdk");
const expect = require("chai").expect;
const models = require("common-lib").models;
const { Organization } = models;
const DATA_CONSTANTS = require("./data/deleteOrganization");
const wrapper = require("../../admin/deleteOrganization");
const sandbox = sinon.createSandbox();
describe("Start Test updateOrganization", () => {
beforeEach(() => {
sandbox.stub(Organization, "update").resolves([1]);
});
afterEach(async () => {
sandbox.restore();
});
it("Test 03: Test to check success returned by handler", async () => {
const mLambda = {
invoke: sinon.stub().returnsThis(),
promise: sinon.stub(),
};
const response = await wrapper.handler(
DATA_CONSTANTS.API_REQUEST_OBJECT_FOR_200
);
console.log({ response });
expect(response.statusCode).to.be.equal(200);
const body = JSON.parse(response.body);
expect(body.message).to.be.equal("Updated successfully");
});
});
Code function
exports.handler = asyncHandler(async (event) => {
InitLambda("userService-deleteOrganization", event);
const { id } = event.pathParameters;
if (isEmpty(id)) {
return badRequest({
message: userMessages[1021],
});
}
try {
const orgrepo = getRepo(Organization);
const [rowsUpdated] = await orgrepo.update(
{ isDeleted: true },
{ org_id: id }
);
if (!rowsUpdated) {
return notFound({
message: userMessages[1022],
});
}
const lambda = new AWS.Lambda({
region: process.env.region,
});
await lambda
.invoke({
FunctionName:
"user-service-" + process.env.stage + "-deleteOrganizationDetail",
InvocationType: "Event",
Payload: JSON.stringify({
pathParameters: { id },
headers: event.headers,
}),
})
.promise();
return success({
message: userMessages[1023],
});
} catch (err) {
log.error(err);
return failure({
error: err,
message: err.message,
});
}
});
It seems that you are not properly stubbing the AWS.Lambda object.
try this,
const sinon = require("sinon");
const AWS = require("aws-sdk");
const expect = require("chai").expect;
const models = require("common-lib").models;
const { Organization } = models;
const DATA_CONSTANTS = require("./data/deleteOrganization");
const wrapper = require("../../admin/deleteOrganization");
const sandbox = sinon.createSandbox();
describe("Start Test updateOrganization", () => {
beforeEach(() => {
sandbox.stub(Organization, "update").resolves([1]);
});
afterEach(async () => {
sandbox.restore();
});
it("Test 03: Test to check success returned by handler", async () => {
const mLambda = { invoke: sinon.stub().returnsThis(), promise: sinon.stub() };
// you missed the below line
sinon.stub(AWS, 'Lambda').callsFake(() => mLambda);
const response = await wrapper.handler(
DATA_CONSTANTS.API_REQUEST_OBJECT_FOR_200
);
console.log({ response });
expect(response.statusCode).to.be.equal(200);
const body = JSON.parse(response.body);
expect(body.message).to.be.equal("Updated successfully");
sinon.assert.calledOnce(AWS.Lambda);
sinon.assert.calledWith(mLambda.invoke, {});
sinon.assert.calledOnce(mLambda.promise);
});
});
I can see that,
You are writing entire logic inside your handler function. This makes it less testable.
To overcome this you can write your code in such a way that is divided into small functions, which are easy to mock in test case files or testable independently. Handler function should only make call to those functions and return the result to the caller.
for Eg.
Lambda Handler:
exports.lambdaHandler = async (event) => {
// do some init work here
const lambdaInvokeResponse = await exports.invokeLambda(params);
}
exports.invokeLambda = async (params) {
const response = await lambda.invoke(params).promise();
return response;
}
test cases:
it('My Test Case - Success', async () => {
const result = await app.lambdaHandler(event);
const invikeLambdaResponse = {
// some dummy response
};
sinon.replace(app, 'invokeLambda', sinon.fake.returns(invikeLambdaResponse ));
});
This is now mocking the only lambda invoke part.
You can mock all the external calls like this (dynamodb, invoke, sns, etc.)
You can set spy and check if the called method is called as per desired arguments

nodejs unit test undefined after required controller

I am try to make a unit test for a controller in a project that use nodeJS, but when I call a function of a controller the test return that this controller is undefined.
This is my MK.js code:
MK = {}
MK.deleteAgent() {
}
module.exports = MK
My test code MK.spec.js:
const assert = require("assert");
const sinon = require("sinon");
const { MK } = require("./MK");
describe("MK controllers", function () {
let rt_model;
beforeEach(function () {
rt_model = {
findAll: sinon.fake.resolves()
}
});
describe("deleteAgent", function () {
it("should call rt_model.findAll", function (done) {
// a mock for the Web response
const response = {
status: () => null,
json: () => {
assert.strictEqual(rt_model.findAll.callCount, 1);
done();
}
};
MK.deleteAgent(null, response);
});
});
afterEach(() => {
// Restore the default sandbox here
sinon.restore();
});
});
But ther result that I get when run test is that MK is undefined

Understand the utility of mocks with Jest

I can't understand at all the utility of mockings. See, I have the next module:
function Code() {
this.generate = () => {
const result = 'code124';
return result;
};
}
module.exports = Code;
Now, I want to test it with jest:
const Code = require('../lib/code');
jest.mock('../lib/code', () => {
return jest.fn().mockImplementation(() => {
return {
generate: () => [1, 2, 3]
};
});
});
describe('Code', () => {
test('returns the code "code123"', () => {
const code = new Code();
expect(code.generate()).toBe('code123');
});
});
So... This test will be fine but... My code ain't so... what's the point about mocking if I can set a correct result even though my code is wrong?
You're NOT supposed to mock the unit you're testing. You're supposed to mock it's dependencies.
for example:
whenever you have a dependency in the implementation:
const dependency = require('dependency');
function Code() {
this.generate = () => {
const result = 'code' + dependency.getNumber();
return result;
};
}
module.exports = Code;
you'll be able to modify it's results to be able to test all scenarios without using the actual implementation of your dependency:
const dependency = require('dependency');
const Code = require('../lib/code');
jest.mock('dependency');
describe('Code', () => {
describe('when dependency returns 123', () => {
beforeAll(() => {
dependency.getNumber.mockReturnValue('123');
});
it('should generate code123', () => {
const code = new Code();
expect(code.generate()).toEqual('code123');
});
});
describe('when dependency returns 124', () => {
beforeAll(() => {
dependency.getNumber.mockReturnValue('124');
});
it('should generate code123', () => {
const code = new Code();
expect(code.generate()).toEqual('code124');
});
});
});

How to do Integration tests NodeJS + Firebase Admin?

I'm trying to write some integration tests on NodeJS with Firebase (firebase-admin, with the test library jest and supertest), and some tests are failing randomly when I run all my tests. Separately, my tests are passing, but it seems like when too many test cases are running, some api calls are failing. Does someone here already had such problem? What are the solutions for this problem? What could cause this problem? (NB: I run my tests sequentially for not mixing up the initialization of my database. I use the option --runInBand with jest)
There are some mocking libraries available, but it seems like they work with the old api of firebase.
Another solution would be to mock all my functions that manipulate firebase, but I won't have a "real" integration test anymore, and it means doing a lot of extra coding for writing those mock. Is it a best practice to do so?
Thank you in advance!
EDIT: code snippet:
initTest.js:
const request = require('supertest');
const net = require('net');
const app = require('../src/server').default;
export const initServer = () => {
const server = net.createServer(function(sock) {
sock.end('Hello world\n');
});
return server
}
export const createAdminAndReturnToken = async (password) => {
await request(app.callback())
.post('/admin/users/sa')
.set('auth','')
.send({password});
// user logs in
const res = await request(app.callback())
.post('/web/users/login')
.set('auth','')
.send({email:"sa#optimetriks.com",password})
return res.body.data.token;
}
utils.ts:
import firestore from "../../src/tools/firestore/index";
export async function execOperations(operations,action,obj) {
if (process.env.NODE_ENV === "test") {
await Promise.all(operations)
.then(() => {
console.log(action+" "+obj+" in database");
})
.catch(() => {
console.log("Error", "error while "+action+"ing "+obj+" to database");
});
} else {
console.log(
"Error",
"cannot execute this action outside from the test environment"
);
}
}
//////////////////////// Delete collections ////////////////////////
export async function deleteAllCollections() {
const collections = ["clients", "web_users","clients_web_users","clients_app_users","app_users"];
collections.forEach(collection => {
deleteCollection(collection);
});
}
export async function deleteCollection(collectionPath) {
const batchSize = 10;
var collectionRef = firestore.collection(collectionPath);
var query = collectionRef.orderBy("__name__").limit(batchSize);
return await new Promise((resolve, reject) => {
deleteQueryBatch(firestore, query, batchSize, resolve, reject);
});
}
async function deleteQueryBatch(firestore, query, batchSize, resolve, reject) {
query
.get()
.then(snapshot => {
// When there are no documents left, we are done
if (snapshot.size == 0) {
return 0;
}
// Delete documents in a batch
var batch = firestore.batch();
snapshot.docs.forEach(doc => {
batch.delete(doc.ref);
});
return batch.commit().then(() => {
return snapshot.size;
});
})
.then(numDeleted => {
if (numDeleted === 0) {
resolve();
return;
}
// Recurse on the next process tick, to avoid
// exploding the stack.
process.nextTick(() => {
deleteQueryBatch(firestore, query, batchSize, resolve, reject);
});
})
.catch(reject);
}
populateClient.ts:
import firestore from "../../src/tools/firestore/index";
import {execOperations} from "./utils";
import { generateClientData } from "../factory/clientFactory";
jest.setTimeout(10000); // some actions here needs more than the standard 5s timeout of jest
// CLIENT
export async function addClient(client) {
const clientData = await generateClientData(client);
await firestore
.collection("clients")
.doc(clientData.id)
.set(clientData)
}
export async function addClients(clientNb) {
let operations = [];
for (let i = 0; i < clientNb; i++) {
const clientData = await generateClientData({});
operations.push(
await firestore
.collection("clients")
.doc(clientData.id)
.set(clientData)
);
}
await execOperations(operations,"add","client");
}
retrieveClient.ts:
import firestore from "../../src/tools/firestore/index";
import { resolveSnapshotData } from "../../src/tools/tools";
export async function getAllClients() {
return new Promise((resolve, reject) => {
firestore
.collection("clients")
.get()
.then(data => {
resolveSnapshotData(data, resolve);
})
.catch(err => reject(err));
});
}
clients.test.js:
const request = require('supertest');
const app = require('../../../src/server').default;
const {deleteAllCollections, deleteCollection} = require('../../../__utils__/populate/utils')
const {addClient} = require('../../../__utils__/populate/populateClient')
const {getAllClients} = require('../../../__utils__/retrieve/retrieveClient')
const {initServer,createAdminAndReturnToken} = require('../../../__utils__/initTest');
const faker = require('faker');
let token_admin;
let _server;
// for simplicity, we use the same password for every users
const password = "secretpassword";
beforeAll(async () => {
_server = initServer(); // start
await deleteAllCollections()
// create a super admin, login and store the token
token_admin = await createAdminAndReturnToken(password);
_server.close(); // stop
})
afterAll(async () => {
// remove the users created during the campaign
_server = initServer(); // start
await deleteAllCollections()
_server.close(); // stop
})
describe('Manage client', () => {
beforeEach(() => {
_server = initServer(); // start
})
afterEach(async () => {
await deleteCollection("clients")
_server.close(); // stop
})
describe('Get All clients', () => {
const exec = (token) => {
return request(app.callback())
.get('/clients')
.set('auth',token)
}
it('should return a 200 when super admin provide the action', async () => {
const res = await exec(token_admin);
expect(res.status).toBe(200);
});
it('should contain an empty array while no client registered', async () => {
const res = await exec(token_admin);
expect(res.body.data.clients).toEqual([]);
});
it('should contain an array with one item while a client is registered', async () => {
// add a client
const clientId = faker.random.uuid();
await addClient({name:"client name",description:"client description",id:clientId})
// call get clients and check the result
const res = await exec(token_admin);
expect(res.body.data.clients.length).toBe(1);
expect(res.body.data.clients[0]).toHaveProperty('name','client name');
expect(res.body.data.clients[0]).toHaveProperty('description','client description');
expect(res.body.data.clients[0]).toHaveProperty('id',clientId);
});
})
describe('Get client by ID', () => {
const exec = (token,clientId) => {
return request(app.callback())
.get('/clients/' + clientId)
.set('auth',token)
}
it('should return a 200 when super admin provide the action', async () => {
const clientId = faker.random.uuid();
await addClient({id:clientId})
const res = await exec(token_admin,clientId);
expect(res.status).toBe(200);
});
it('should return a 404 when the client does not exist', async () => {
const nonExistingClientId = faker.random.uuid();
const res = await exec(token_admin,nonExistingClientId);
expect(res.status).toBe(404);
});
})
describe('Update client', () => {
const exec = (token,clientId,client) => {
return request(app.callback())
.patch('/clients/' + clientId)
.set('auth',token)
.send(client);
}
const clientModified = {
name:"name modified",
description:"description modified",
app_user_licenses: 15
}
it('should return a 200 when super admin provide the action', async () => {
const clientId = faker.random.uuid();
await addClient({id:clientId})
const res = await exec(token_admin,clientId,clientModified);
expect(res.status).toBe(200);
// check if the client id modified
let clients = await getAllClients();
expect(clients.length).toBe(1);
expect(clients[0]).toHaveProperty('name',clientModified.name);
expect(clients[0]).toHaveProperty('description',clientModified.description);
expect(clients[0]).toHaveProperty('app_user_licenses',clientModified.app_user_licenses);
});
it('should return a 404 when the client does not exist', async () => {
const nonExistingClientId = faker.random.uuid();
const res = await exec(token_admin,nonExistingClientId,clientModified);
expect(res.status).toBe(404);
});
})
describe('Create client', () => {
const exec = (token,client) => {
return request(app.callback())
.post('/clients')
.set('auth',token)
.send(client);
}
it('should return a 200 when super admin does the action', async () => {
const res = await exec(token_admin,{name:"clientA",description:"description for clientA"});
expect(res.status).toBe(200);
});
it('list of clients should be appended when a new client is created', async () => {
let clients = await getAllClients();
expect(clients.length).toBe(0);
const res = await exec(token_admin,{name:"clientA",description:"description for clientA"});
expect(res.status).toBe(200);
clients = await getAllClients();
expect(clients.length).toBe(1);
expect(clients[0]).toHaveProperty('name','clientA');
expect(clients[0]).toHaveProperty('description','description for clientA');
});
});
describe('Delete client', () => {
const exec = (token,clientId) => {
return request(app.callback())
.delete('/clients/'+ clientId)
.set('auth',token);
}
it('should return a 200 when super admin does the action', async () => {
const clientId = faker.random.uuid();
await addClient({id:clientId})
const res = await exec(token_admin,clientId);
expect(res.status).toBe(200);
});
it('should return a 404 when trying to delete a non-existing id', async () => {
const clientId = faker.random.uuid();
const nonExistingId = faker.random.uuid();
await addClient({id:clientId})
const res = await exec(token_admin,nonExistingId);
expect(res.status).toBe(404);
});
it('the client deleted should be removed from the list of clients', async () => {
const clientIdToDelete = faker.random.uuid();
const clientIdToRemain = faker.random.uuid();
await addClient({id:clientIdToRemain})
await addClient({id:clientIdToDelete})
let clients = await getAllClients();
expect(clients.length).toBe(2);
await exec(token_admin,clientIdToDelete);
clients = await getAllClients();
expect(clients.length).toBe(1);
expect(clients[0]).toHaveProperty('id',clientIdToRemain);
});
});
})
jest command: jest --coverage --forceExit --runInBand --collectCoverageFrom=src/**/*ts
I found the problem: I had a problem on the "deleteAllCollection" function, I forgot to put an "await".
Here is the correction for this function:
export async function deleteAllCollections() {
const collections = ["clients", "web_users","clients_web_users","clients_app_users","app_users"];
for (const collection of collections) {
await deleteCollection(collection);
};
}

Resources