Mock api request Jest NodeJs - node.js

I'm trying to test the following code:
adapter.js
async function adapt(message) {
let parser = JSON.parse(message.content.toString());
let apiResult = await api(parser.id);
let result = apiResult.data.data;
return adapptedMessage = {"id": result.id}
}
This is my api call.
server.js
const axios = require('axios');
const url = process.env.URL;
function getApi(id) {
return axios.get(url + id).catch(function (error) {
if (error.response) {
// Request made and server responded
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
// The request was made but no response was received
console.log(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.log('Error', error.message);
}});
}
module.exports = getApi
This is how I tried to test.
test.js
jest.mock('./server');
const axios = require('axios');
const adapt = require('./adapter');
describe("Adapter Test", () => {
test("adapt", async () => {
var result = await adapt(getMessage());
const mockResp = {"data":{"data": {"id":10}}};
axios.get = jest.fn(() => mockResp);
assert
expect(result).toStrictEqual(getOfferMessage());
});
})
function getMessage() {
return {"content":"{\"id\":10}"};
}
This is my first test in js, and I don't know how to mock the api call.
All I get is "undefined".
Could you help me?
Thanks

You should pass a factory function to jest.mock when mocking your server module
const mockResp = {"data":{"data": {"id":10}}};
jest.mock('./server', () => () => mockResp);
const adapt = require('./adapter');
describe("Adapter Test", () => {
test("adapt", async () => {
const result = await adapt(getMessage());
expect(result).toStrictEqual({ id: 10 });
});
})
function getMessage() {
return {"content":"{\"id\":10}"};
}

Related

Mocking function to unit test Serverless Lambda

I am really struggling to understand unit testing within a Serverless Application. So I obviously have my handler, and I have a single Lambda function
const responses = require('../utils/jsonResponse');
const someConnector = require('../services/connectToService/connectToService');
module.exports = async (event) => {
const connectionParams = {
//some env variables
};
try {
const token = await someConnector.connectToService(connectionParams);
return responses.status(token, 200);
} catch (e) {
return responses.status(
`Issue connecting to service - ${e.message}`,
500,
);
}
};
So this Lambda function is pretty straight forward, gets some environment variables, and awaits a response from a service. It then returns the response.
So I have already done integration tests for this which is fine, but now I wanted to do a Unit test. I wanted to test this function in isolation, so essentially I want to mock connectToService to return my own responses.
So I came up with the following
require('dotenv').config();
const { expect } = require('chai');
const sinon = require('sinon');
let sandbox = require("sinon").createSandbox();
const LambdaTester = require('lambda-tester');
const handler = require('../../../handler');
const msConnector = require('../../../services/connectToService/connectToService');
describe('Testing handler', async (done) => {
describe('endpoint someEndpoint returns 200', () => {
it('Should resolve with 200', async () => {
before(() => {
sandbox = sinon.createSandbox();
sandbox.stub(msConnector, 'connectToService').resolves('some-token');
});
afterEach(() => {
sandbox.restore();
});
await LambdaTester(handler.someEndpoint)
.expectResult((result) => {
console.log(result);
expect(result.statusCode).to.equal(200);
});
});
});
done();
});
msConnector is the filename of the service, connectToService is the function name. What I want to do is not invoke this function, but return some-token when my Lambda calls it.
However, I have the console.log, and what I get from that is the real token, not some-token.
This tells me that the mocked function is really being called and executed and returning the real value.
So how can I mock this to make sure it returns some-token?
Thanks
Service function
const { DOMParser } = require('#xmldom/xmldom');
const axios = require('axios');
const { loginRequest } = require('./xml/login');
const connectToService = async (connectionParams) => {
//this injects config details into XML
const xmlRequest = loginRequest(
connectionParams.username,
connectionParams.password,
connectionParams.url,
);
const config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': xmlRequest.length,
},
};
const token = await axios
.post(connectionParams.msHost, xmlRequest, config)
.then((res) => {
const dom = new DOMParser().parseFromString(res.data, 'text/xml');
if (
dom.documentElement
.getElementsByTagName('wsse:secToken')
.item(0)
) {
return dom.documentElement
.getElementsByTagName('wsse:secToken')
.item(0).firstChild.nodeValue;
}
throw new Error('Invalid Username/Password');
})
.catch((err) => {
throw new Error(`Error making connection - ${err.message}`);
});
return token;
};
module.exports = {
connectToService,
};
The function connectToService may be not same copy between you mocked and called.
Because you overwrote a new object by module.exports = .... This causes you probably get different object for each require.
Try to do the below approach sharing the same object for all require.
const { DOMParser } = require('#xmldom/xmldom');
const axios = require('axios');
const { loginRequest } = require('./xml/login');
const connectToService = async (connectionParams) => {
//this injects config details into XML
const xmlRequest = loginRequest(
connectionParams.username,
connectionParams.password,
connectionParams.url,
);
const config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': xmlRequest.length,
},
};
const token = await axios
.post(connectionParams.msHost, xmlRequest, config)
.then((res) => {
const dom = new DOMParser().parseFromString(res.data, 'text/xml');
if (
dom.documentElement
.getElementsByTagName('wsse:secToken')
.item(0)
) {
return dom.documentElement
.getElementsByTagName('wsse:secToken')
.item(0).firstChild.nodeValue;
}
throw new Error('Invalid Username/Password');
})
.catch((err) => {
throw new Error(`Error making connection - ${err.message}`);
});
return token;
};
module.exports.connectToService = connectToService;

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 with supertest, calling an endpoint twice for different test cases getting a Did you forget to wait for something async in your test?

i have a single endpoint where i call twice on 2 different describes to test different responses
const express = require("express");
const router = express.Router();
const fs = require("fs");
const multer = require("multer");
const upload = multer({ dest: "files/" });
const csv = require("fast-csv");
let response = { message: "success" }
router.post("/post_file", upload.single("my_file"), (req, res) => {
let output = get_output(req.file.path);
fs.unlinkSync(req.file.path);
if(output.errors.length > 0) response.message = "errors found";
res.send(JSON.stringify(response))
})
const get_output = (path) => {
let errors = []
let fs_stream = fs.createReadStream(path);
let csv_stream = csv.parse().on("data", obj => {
if(!is_valid(obj)) errors.push(obj);
});
fs_stream.pipe(csv_stream);
return {errors};
}
const is_valid = (row) => {
console.log("validate row")
// i validate here and return a bool
}
my unit tests
const app = require("../server");
const supertest = require("supertest");
const req = supertest(app);
describe("parent describe", () => {
describe("first call", () => {
const file = "my path to file"
// this call succeeds
it("should succeed", async (done) => {
let res = await req
.post("/post_file")
.attach("my_file", file);
expect(JSON.parse(res.text).message).toBe("success")
done();
});
})
describe("second call", () => {
const file = "a different file"
// this is where the error starts
it("should succeed", async (done) => {
let res = await req
.post("/post_file")
.attach("my_file", file);
expect(JSON.parse(res.text).message).toBe("errors found")
done();
});
})
})
// csv file is this
NAME,ADDRESS,EMAIL
Steve Smith,35 Pollock St,ssmith#emailtest.com
I get the following
Cannot log after tests are done. Did you forget to wait for something async in your test?
Attempted to log "validate row".
The problem is that tested route is incorrectly implemented, it works asynchronously but doesn't wait for get_output to end and responds synchronously with wrong response. The test just reveals that console.log is asynchronously called after test end.
Consistent use of promises is reliable way to guarantee the correct execution order. A stream needs to be promisified to be chained:
router.post("/post_file", upload.single("my_file"), async (req, res, next) => {
try {
let output = await get_output(req.file.path);
...
res.send(JSON.stringify(response))
} catch (err) {
next(err)
}
})
const get_output = (path) => {
let errors = []
let fs_stream = fs.createReadStream(path);
return new Promise((resolve, reject) => {
let csv_stream = csv.parse()
.on("data", obj => {...})
.on("error", reject)
.on("end", () => resolve({errors}))
});
}
async and done shouldn't be mixed in tests because they serve the same goal, this may result in test timeout if done is unreachable:
it("should succeed", async () => {
let res = await req
.post("/post_file")
.attach("my_file", file);
expect(JSON.parse(res.text).message).toBe("success")
});

Sinon stub not working if tested using express app

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.

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