Jest Mock FS File Stream - node.js

I am very new to jest and mocking, I have a fs module read stream, which has 2 events on and data, i am trying to mock below code
ReadFile.js
const csv = require('csv-parser');
let storeData=[];
csvFileReader() {
fs.createReadStream(path.resolve(__dirname, "./abc.csv"))
.pipe(csv())
.on('data', async (row) => {
storeData[0] = row.postcode;
})
.on('end', () => {
console.log('Done')
});
}
ReadFileTest.js
import ReadFile from './readFile.js';
const fs = require('fs');
jest.mock('fs');
describe('Load File', () => {
const readFile= new ReadFile();
test('Test data handler', async () => {
const mockPipeOn = { on: jest.fn().mockImplementation(function(this, event, handler) {
if (event === 'data') {
jest.fn.mockReturnValueOnce("Reading Data")
}
if (event === 'end') {
jest.fn.mockReturnValueOnce("Completed Reading Data")
}
return this;
}), };
const mockReadStream = { pipe: jest.fn().mockReturnValueOnce(mockPipeOn) };
const createReadStream = jest.fn().mockReturnValueOnce(mockReadStream);
await readFile.csvFileReader();
});
});
I am getting error on 'this' key word its not going into 'data' and 'end' error handler

You should use mockFn.mockReturnThis() to return the context.
E.g.
ReadFile.js:
import fs from 'fs';
import path from 'path';
import csv from 'csv-parser';
class ReadFile {
csvFileReader() {
fs.createReadStream(path.resolve(__dirname, './abc.csv'))
.pipe(csv())
.on('data', async (row) => {
console.log('Storing Data');
})
.on('end', () => {
console.log('Done');
});
}
}
export default ReadFile;
ReadFile.test.js
import ReadFile from './ReadFile';
import fs from 'fs';
jest.mock('fs');
describe('67216891', () => {
const readFile = new ReadFile();
it('should store', () => {
const mReadStream = {
pipe: jest.fn().mockReturnThis(),
on: jest.fn().mockImplementation(function (event, handler) {
handler();
return this;
}),
};
fs.createReadStream.mockReturnValueOnce(mReadStream);
readFile.csvFileReader();
expect(fs.createReadStream).toBeCalledTimes(1);
expect(mReadStream.pipe).toBeCalledTimes(1);
expect(mReadStream.on).toBeCalledWith('data', expect.any(Function));
expect(mReadStream.on).toBeCalledWith('end', expect.any(Function));
});
});
test result:
PASS examples/67216891/ReadFile.test.js (8.891 s)
67216891
✓ should store (20 ms)
console.log
Storing Data
at ReadFile.<anonymous> (examples/67216891/ReadFile.js:10:17)
console.log
Done
at examples/67216891/ReadFile.js:13:17
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
ReadFile.js | 100 | 100 | 100 | 100 |
-------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 10.159 s

Related

Mocked Jest callback method not called

I'm testing the NodeJS function/module below using jestjs
const fs = require("fs");
const path = require("path");
function read(filename, callback) {
fs.readFile(path.join(__dirname, filename), { encoding: "utf-8" }, callback);
}
module.exports = {
read,
};
The first test is:
test("callback data returned is correct", (done) => {
function callback(err, data) {
try {
expect(data).toBe("1");
done();
} catch (err) {
done(err);
}
}
read("./test", callback);
});
and is succesful (I have an actual real test file with the contents being 1.
The second one is a bit trickier. Since I'm providing a mockCallback function to read, why isn't the callback called?
const { read } = require("./callback");
describe("callback testing", () => {
test("callback is called", (done) => {
const mockCallback = jest.fn((err, data) => data);
read("./test", mockCallback);
expect(mockCallback).toHaveBeenCalled();
done();
});
});
The test fails and while inspecting the mock function it seems it wasn't called.
I also tried adding the following in order to mock fs.readFile, without much success:
const mock = require("mock-fs");
const fs = require("fs");
jest.mock("fs");
beforeEach(() => {
mock({
test: "1",
});
});
I'd like a solution that mocks the least of the dependant methods and an explanation as to why isn't the callback run.
You should mock fs.readFile method and trigger the callback in your test case.
E.g.
callback.js:
const fs = require('fs');
const path = require('path');
function read(filename, callback) {
fs.readFile(path.join(__dirname, filename), { encoding: 'utf-8' }, callback);
}
module.exports = {
read,
};
callback.test.js:
const { read } = require('./callback');
const fs = require('fs');
describe('callback testing', () => {
test('callback is called', () => {
const mockCallback = jest.fn();
const mockData = 'mock file data';
const mockReadFile = jest.spyOn(fs, 'readFile').mockImplementationOnce((filename, options, callback) => {
callback(null, mockData);
});
read('./test', mockCallback);
expect(mockCallback).toHaveBeenCalled();
expect(mockReadFile).toBeCalledWith(expect.any(String), { encoding: 'utf-8' }, mockCallback);
mockReadFile.mockRestore();
});
});
unit test result:
PASS examples/65361608/callback.test.js
callback testing
✓ callback is called (3 ms)
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
callback.js | 100 | 100 | 100 | 100 |
-------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.992 s, estimated 5 s

Mocking pump node_module with different implementations using jest - Typescript

I am trying to implement gcloud-storage with nodejs and test them using typescript
This is my actual class
Please do not consider the logging implementation for now.
The storage is authenticated by an external service call -
const str =
GcloudAuthenticationInstance.createGcloudAuthenticationBucket();
and the file that I am willing to store in gcloud is manipulated using streams , with the pump module
export const uploadEnvFiles = async (env_name: string) => {
const LOGGER: pino.Logger = PinoLoggerServiceInstance.getLogger(__filename);
return new Promise(async (res, rej) => {
const str = GcloudAuthenticationInstance.createGcloudAuthenticationBucket();
const bucketToUpload = GCLOUD_ENV_STR_BUCKET_NAME;
let uploadLocalFilePath;
let destinationBucketPath;
if (!AppUtilServiceInstance.isNullOrUndefined(env_name)) {
uploadLocalFilePath = ENV_NAME_DEV === env_name ? GCLOUD_UPLOAD_FILE_DEV_LOCAL_PATH : GCLOUD_UPLOAD_FILE_PROD_LOCAL_PATH;
destinationBucketPath = ENV_NAME_DEV === env_name ? GCLOUD_DATABASE_BUCKET_DEV : GCLOUD_DATABASE_BUCKET_PROD;
}
LOGGER.info('after authentication');
pump(
fs.createReadStream(uploadLocalFilePath),
str
.bucket(bucketToUpload)
.file(destinationBucketPath)
.createWriteStream({
gzip: true,
public: true,
resumable: true,
})
)
.on('error', (err) => {
LOGGER.error('Error occured in uploading:', err);
rej({ status: 'Error', error: err, code: 500 });
})
.on('finish', () => {
LOGGER.info('Successfully uploaded the file');
res({ status: 'Success', code: 201, error: null });
});
});
};
Now there are possibilities of the stream finishing or erroring out and I wanted to test both.
I am able to mock the pump npm module as a whole with jest.mock like this hoisted at the top before any test suite declarations.
jest.mock('pump', () =>
jest.fn().mockImplementation(() => {
const readStream = fs.createReadStream(
path.resolve(process.cwd(), './tests/cloud-storage/sample-read.txt')
);
const writeStream = fs.createWriteStream(
path.resolve(process.cwd(), './tests/cloud-storage/sample-write.txt')
);
return readStream.pipe(writeStream);
})
);
So the above is an implementation for the working scenario, where I have piped an existing file to an output stream and returned the stream, making the mock of pump to work. Here is my test spec file
const globalAny: any = global;
describe('Test suite for bucket functionality', () => {
beforeEach(() => {
jest.restoreAllMocks();
});
afterAll(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
jest.resetAllMocks();
});
test('test upload - make the actual call', async (done) => {
// to make sure that mock fs doesnt affect the gcloud authentication, this is a MUST
const createGcloudAuthenticationBucketSpy = jest
.spyOn(GcloudAuthenticationInstance, 'createGcloudAuthenticationBucket')
.mockImplementation(() => {
return new Storage();
});
const res = BucketOperations.uploadEnvFiles(globalAny.ENV_JEST);
await expect(res).resolves.toBeDefined();
expect(createGcloudAuthenticationBucketSpy).toHaveBeenCalledTimes(1);
done();
});
});
Now this works with the mocked pump call. But I wanted to test the scenario where the stream emits error as well in the same spec. Is there a possibility to overwrite the mockImplementation in another test spec. Since this is a npm module, I have written the jest.mock() at the top which will serve as the mock for the entire test suite, but unsure as to how to overwrite it. I've been trying for past 3 days and couldn't figure it out. Any way that can be achieved?
Here is the unit test solution using jest.mock(moduleName, factory, options) and jest.spyOn(object, methodName).
bucketOperations.ts:
import fs from 'fs';
import pump from 'pump';
import { GcloudAuthenticationInstance } from './gcloudAuthenticationInstance';
import { AppUtilServiceInstance } from './appUtilServiceInstance';
const {
GCLOUD_ENV_STR_BUCKET_NAME,
GCLOUD_UPLOAD_FILE_DEV_LOCAL_PATH,
GCLOUD_UPLOAD_FILE_PROD_LOCAL_PATH,
GCLOUD_DATABASE_BUCKET_DEV,
GCLOUD_DATABASE_BUCKET_PROD,
ENV_NAME_DEV,
} = process.env;
export const uploadEnvFiles = async (env_name: string) => {
return new Promise(async (res, rej) => {
const str = GcloudAuthenticationInstance.createGcloudAuthenticationBucket();
const bucketToUpload = GCLOUD_ENV_STR_BUCKET_NAME;
let uploadLocalFilePath;
let destinationBucketPath;
if (!AppUtilServiceInstance.isNullOrUndefined(env_name)) {
uploadLocalFilePath =
ENV_NAME_DEV === env_name ? GCLOUD_UPLOAD_FILE_DEV_LOCAL_PATH : GCLOUD_UPLOAD_FILE_PROD_LOCAL_PATH;
destinationBucketPath = ENV_NAME_DEV === env_name ? GCLOUD_DATABASE_BUCKET_DEV : GCLOUD_DATABASE_BUCKET_PROD;
}
console.info('after authentication');
pump(
fs.createReadStream(uploadLocalFilePath),
str
.bucket(bucketToUpload)
.file(destinationBucketPath)
.createWriteStream({
gzip: true,
public: true,
resumable: true,
}),
)
.on('error', (err) => {
console.error('Error occured in uploading:', err);
rej({ status: 'Error', error: err, code: 500 });
})
.on('finish', () => {
console.info('Successfully uploaded the file');
res({ status: 'Success', code: 201, error: null });
});
});
};
appUtilServiceInstance.ts:
const AppUtilServiceInstance = {
isNullOrUndefined: (env_name) => typeof env_name === 'undefined',
};
export { AppUtilServiceInstance };
gcloudAuthenticationInstance.ts:
const GcloudAuthenticationInstance = {
createGcloudAuthenticationBucket: () => {
const storage = {
bucket(name) {
return this;
},
file(filename) {
return this;
},
createWriteStream(options) {
return 'write stream';
},
};
return storage;
},
};
export { GcloudAuthenticationInstance };
bucketOperations.test.ts:
import pump from 'pump';
import fs from 'fs';
import { GcloudAuthenticationInstance } from './gcloudAuthenticationInstance';
jest.mock('pump', () => {
const mPump = { on: jest.fn() };
return jest.fn(() => mPump);
});
describe('61031410', () => {
let originalEnv;
beforeEach(() => {
originalEnv = process.env;
});
afterEach(() => {
process.env = originalEnv;
jest.restoreAllMocks();
});
it('should upload file correctly', async () => {
process.env.ENV_NAME_DEV = 'dev';
process.env.GCLOUD_ENV_STR_BUCKET_NAME = 'bucket-dev';
process.env.GCLOUD_UPLOAD_FILE_DEV_LOCAL_PATH = 'dev';
process.env.GCLOUD_DATABASE_BUCKET_DEV = 'bucket-dev-db';
const BucketOperations = require('./bucketOperations');
const createReadStreamSpy = jest.spyOn(fs, 'createReadStream').mockReturnValueOnce('rs' as any);
const mStorage: any = {
bucket: jest.fn().mockReturnThis(),
file: jest.fn().mockReturnThis(),
createWriteStream: jest.fn().mockReturnValueOnce('ws'),
};
const infoSpy = jest.spyOn(console, 'info');
const createGcloudAuthenticationBucketSpy = jest
.spyOn(GcloudAuthenticationInstance, 'createGcloudAuthenticationBucket')
.mockReturnValueOnce(mStorage);
pump().on.mockImplementation(function(this: any, event, callback) {
if (event === 'finish') {
callback();
}
return this;
});
const actual = await BucketOperations.uploadEnvFiles('dev');
expect(actual).toEqual({ status: 'Success', code: 201, error: null });
expect(createGcloudAuthenticationBucketSpy).toBeCalledTimes(1);
expect(pump).toBeCalledWith('rs', 'ws');
expect(createReadStreamSpy).toBeCalledWith('dev');
expect(mStorage.bucket).toBeCalledWith('bucket-dev');
expect(mStorage.file).toBeCalledWith('bucket-dev-db');
expect(mStorage.createWriteStream).toBeCalledWith({ gzip: true, public: true, resumable: true });
expect(infoSpy.mock.calls[0]).toEqual(['after authentication']);
expect(infoSpy.mock.calls[1]).toEqual(['Successfully uploaded the file']);
});
it('should handle the error if upload file failure', () => {
// TODO: you can do this like above
});
});
unit test results with coverage report:
PASS stackoverflow/61031410/bucketOperations.test.ts (7.94s)
61031410
✓ should upload file correctly (69ms)
✓ should handle the error if upload file failure
console.info node_modules/jest-environment-enzyme/node_modules/jest-mock/build/index.js:866
after authentication
console.info node_modules/jest-environment-enzyme/node_modules/jest-mock/build/index.js:866
Successfully uploaded the file
---------------------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------------------------|---------|----------|---------|---------|-------------------
All files | 80.56 | 50 | 54.55 | 79.41 |
appUtilServiceInstance.ts | 100 | 100 | 100 | 100 |
bucketOperations.ts | 92.31 | 50 | 83.33 | 91.67 | 40,41
gcloudAuthenticationInstance.ts | 28.57 | 100 | 0 | 28.57 | 3,5,8,11,14
---------------------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 9.247s
source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/61031410

How can I mock a single method from a mocked class in jest?

I am using node 12 and jest for unit testing. I have a code to open connection via websocket.
const ws = require('ws');
this.ws = new WebSocket(url);
this.ws.on('open', () => {
// How to test this callback?
...
resolve();
});
this.ws.on('error', (err) => {
// How to test this callback?
...
reject(err)
});
In my test case, I have mocked ws module via jtest:
const WebSocket = require('ws');
jest.mock('ws')
test('quote server listener should be able to connect to quote server', () => {
const server = new QuoteServerListener(null, 'http://mock.com');
server.connect();
const mockWSInstance = WebSocket.mock.instances[0];
expect(mockWSInstance.on).toHaveBeenCalledTimes(1);
});
The above test case works fine. But I don't know how to trigger a call to the callback function on ws.on('open', () => .... I'd like to test the logic when the connection is open. How can I achieve this in the mock?
I have tried to emit a open event via mockWSInstance instance like mockWSInstance.emit('open', null) but it doesn't trigger the code. What should I do in this case?
Here is the unit test solution:
index.ts:
export class SomeClass {
ws;
run() {
const WebSocket = require("ws");
const url = "";
this.ws = new WebSocket(url);
return new Promise((resolve, reject) => {
this.ws.on("open", () => {
resolve();
});
this.ws.on("error", err => {
reject(err);
});
});
}
}
index.spec.ts:
import { SomeClass } from "./";
const WebSocket = require("ws");
jest.mock("ws", () => {
const mWebSocket = {
on: jest.fn()
};
return jest.fn(() => mWebSocket);
});
describe("SomeClass", () => {
let instance;
let ws;
beforeEach(() => {
ws = new WebSocket();
instance = new SomeClass();
});
afterAll(() => {
jest.resetAllMocks();
});
it("should pass", async () => {
const eventHandler = {};
ws.on.mockImplementation((event, handler) => {
eventHandler[event] = handler;
});
const pending = instance.run();
eventHandler["open"]();
const actual = await pending;
expect(actual).toBeUndefined();
expect(ws.on).toBeCalledWith("open", eventHandler["open"]);
});
it("should fail", async () => {
const eventHandler = {};
ws.on.mockImplementation((event, handler) => {
eventHandler[event] = handler;
});
const pending = instance.run();
const mError = new Error("connection error");
eventHandler["error"](mError);
await expect(pending).rejects.toThrowError(mError);
expect(ws.on).toBeCalledWith("error", eventHandler["error"]);
});
});
Unit test result with 100% coverage:
PASS src/stackoverflow/59084313/index.spec.ts
SomeClass
✓ should pass (4ms)
✓ should fail (3ms)
----------|----------|----------|----------|----------|-------------------|
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: 2 passed, 2 total
Snapshots: 0 total
Time: 3.781s, estimated 8s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/59084313

How to mock event and event handler for node.js module?

Here is my function
async function remoteCSVToJSON<T>(url: string, options: csv.ConverterOptions) {
const defaultOptions = {
noheader: false,
delimiter: ',',
workerNum: os.cpus().length,
};
const finalOptions = defaultsDeep(options, defaultOptions);
const datas: T[] = [];
return new Promise<T[]>((resolve, reject) => {
request.get(url).then(res => {
csv(finalOptions)
.fromString(res)
.on('json', (jsonObj: T) => datas.push(jsonObj))
.on('error', err => reject(err))
.on('end', () => {
logger.info('convert csv to json done');
resolve(datas);
});
});
});
}
I can mock fromString for csvtojson module like this:
jest.mock('csvtojson', () => {
return {
__esModule: true,
fromString: jest.fn(),
};
});
But how do I mock the .on(event) method and event handlers?
Here is the unit test solution:
index.ts:
import request from 'request-promise';
import csv from 'csvtojson';
import os from 'os';
import { defaultsDeep } from 'lodash';
export async function remoteCSVToJSON<T>(url: string, options: any) {
const defaultOptions = {
noheader: false,
delimiter: ',',
workerNum: os.cpus().length
};
const finalOptions = defaultsDeep(options, defaultOptions);
const datas: T[] = [];
return new Promise<T[]>((resolve, reject) => {
request.get(url).then(res => {
csv(finalOptions)
.fromString(res)
.on('json', (jsonObj: T) => datas.push(jsonObj))
.on('error', err => reject(err))
.on('end', () => {
console.info('convert csv to json done');
resolve(datas);
});
});
});
}
index.spec.ts:
import request from 'request-promise';
import csv from 'csvtojson';
import os from 'os';
import { remoteCSVToJSON } from './';
jest.mock('csvtojson', () => {
const mCsv = {
on: jest.fn(),
fromString: jest.fn().mockReturnThis()
};
return jest.fn(() => mCsv);
});
jest.mock('request-promise', () => {
return {
get: jest.fn()
};
});
describe('remoteCSVToJSON', () => {
afterEach(() => {
jest.restoreAllMocks();
});
test('should request and convert csv to json', async () => {
const events: any = {};
csv()
.fromString()
.on.mockImplementation(function(this: any, ...args: any[]) {
const [event, handler] = args;
events[event] = handler;
return this;
});
const mGetResponse = { data: 'fake data' };
(request.get as jest.MockedFunction<typeof request.get>).mockResolvedValueOnce(mGetResponse);
const infoSpy = jest.spyOn(console, 'info');
const actualValue = remoteCSVToJSON('url', {});
const mJsonObj = { id: 1, name: 'mrdulin' };
expect(jest.isMockFunction(csv)).toBeTruthy();
await new Promise(resolve => setTimeout(resolve, 0));
expect(csv).toBeCalledWith({
noheader: false,
delimiter: ',',
workerNum: os.cpus().length
});
expect(csv().fromString).toBeCalledWith(mGetResponse);
events['json'](mJsonObj);
events['end']();
await expect(actualValue).resolves.toEqual([mJsonObj]);
expect(request.get).toBeCalledWith('url');
expect(infoSpy).toBeCalledWith('convert csv to json done');
});
test('should throw error when convert error', async () => {
const events: any = {};
csv()
.fromString()
.on.mockImplementation(function(this: any, ...args: any[]) {
const [event, handler] = args;
events[event] = handler;
return this;
});
const mGetResponse = { data: 'fake data' };
(request.get as jest.MockedFunction<typeof request.get>).mockResolvedValueOnce(mGetResponse);
const actualValue = remoteCSVToJSON('url', {});
const mJsonObj = { id: 1, name: 'mrdulin' };
expect(jest.isMockFunction(csv)).toBeTruthy();
await new Promise(resolve => setTimeout(resolve, 0));
expect(csv).toBeCalledWith({
noheader: false,
delimiter: ',',
workerNum: os.cpus().length
});
expect(csv().fromString).toBeCalledWith(mGetResponse);
events['json'](mJsonObj);
const mError = new Error('mock error');
events['error'](mError);
await expect(actualValue).rejects.toThrowError(mError);
expect(request.get).toBeCalledWith('url');
});
});
Unit test result with 100% coverage:
PASS src/stackoverflow/57423762/index.spec.ts (6.995s)
remoteCSVToJSON
✓ should request and convert csv to json (16ms)
✓ should throw error when convert error (22ms)
console.info node_modules/jest-mock/build/index.js:860
convert csv to json done
----------|----------|----------|----------|----------|-------------------|
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: 2 passed, 2 total
Snapshots: 0 total
Time: 8.114s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/57423762

nodejs - sinon stub not being called

For some reason I am having a little trouble getting this simple test to run correctly with a similar set-up I have used multiple times before.
Perhaps a fresh pair of eyes could help me understand why my method generateReport is not being invoked and none of my stubs are being triggered with the intended arguments?
BYE nor GOOD are ever logged and the tests are returning the error: AssertError: expected stub to be called with arguments
My index file:
const errorHandler = require('./lib/handlers/error-handler')
const transformRequest = require('./lib/handlers/request-converter')
const convert = require('./lib/convert')
exports.generateReport = function generateReport(req, res) {
console.log('HELLO')
const objectToPopulateTemplate = transformRequest(req.body)
convert(objectToPopulateTemplate, function (e, data) {
if (e) {
console.log('BYE')
const error = errorHandler(e)
return res.send(error.httpCode).json(error)
}
console.log('GOOD')
res
.set('Content-Type', 'application/pdf')
.set('Content-Disposition', `attachment; filename=velocity_report_${new Date()}.pdf`)
.set('Content-Length', data.length)
.status(200)
.end(data)
})
}
My test file:
const proxyquire = require('proxyquire')
const assert = require('assert')
const sinon = require('sinon')
const fakeData = require('./data/sample-request.json')
describe('report-exporter', () => {
describe('generateReport', () => {
const fakeError = new Error('Undefined is not a function')
let res, resSendStub, resStatusStub, resEndStub, resSetStub, resJsonStub, req, convertStub, generate
before(() => {
resSendStub = sinon.stub()
resJsonStub = sinon.stub()
resStatusStub = sinon.stub()
resEndStub = sinon.stub()
resSetStub = sinon.stub()
convertStub = sinon.stub()
res = {
send: function(errorCode) {
return resSendStub(errorCode)
},
json: function(object) {
return resJsonStub(object)
},
set: function(arg1, arg2) {
return resSetStub(arg1, arg2)
},
status: function(code) {
return resStatusStub(code)
},
end: function(data) {
return resEndStub(data)
}
}
req = {
body: {}
}
generate = proxyquire('./../index', {
'./lib/convert': function() {
return convertStub
}
})
})
it('Should return an error response', () => {
convertStub.throws(fakeError)
generate.generateReport(req, res)
sinon.assert.calledWith(resSendStub, '500')
})
})
})
It looks like you are proxyquireing your ./lib/convert incorrectly. Original convert is called with objectToPopulateTemplate and a callback function (e, data). And that's the callback who is responsible for error handling and sending a response.
The stubbed convert function though doesn't care about about the provided callback at all, so the callback never gets called.
Most likely what you want is to pass the parameters to convertStub and deal with them later:
'./lib/convert': function(objectToPopulateTemplate, cb) {
return convertStub(objectToPopulateTemplate, cb);
}
and then
it('Should return an error response', () => {
generate.generateReport(req, res);
const cb = convertStub.getCall(0).args[1];
// simulate `convert` to fail
cb(fakeError);
sinon.assert.calledWith(resSendStub, '500')
})
Here is the unit test solution:
index.js:
const errorHandler = require("./error-handler");
const transformRequest = require("./request-converter");
const convert = require("./convert");
exports.generateReport = function generateReport(req, res) {
console.log("HELLO");
const objectToPopulateTemplate = transformRequest(req.body);
convert(objectToPopulateTemplate, function(e, data) {
if (e) {
console.log("BYE");
const error = errorHandler(e);
return res.send(error.httpCode).json(error);
}
console.log("GOOD");
res
.set("Content-Type", "application/pdf")
.set(
"Content-Disposition",
`attachment; filename=velocity_report_${new Date()}.pdf`
)
.set("Content-Length", data.length)
.status(200)
.end(data);
});
};
error-handler.js:
module.exports = function(error) {
return error;
};
request-converter.js:
module.exports = function transformRequest(body) {
return body;
};
convert.js:
module.exports = function convert(body, callback) {
callback();
};
index.spec.js:
const sinon = require("sinon");
const proxyquire = require("proxyquire");
describe("report-exporter", () => {
describe("generateReport", () => {
afterEach(() => {
sinon.restore();
});
const fakeError = new Error("Undefined is not a function");
fakeError.httpCode = 500;
it("Should return an error response", () => {
const logSpy = sinon.spy(console, "log");
const mReq = { body: {} };
const mRes = { send: sinon.stub().returnsThis(), json: sinon.stub() };
const convertStub = sinon.stub();
const errorHandlerStub = sinon.stub().returns(fakeError);
const transformRequestStub = sinon.stub().returns(mReq.body);
const generate = proxyquire("./", {
"./convert": convertStub,
"./error-handler": errorHandlerStub,
"./request-converter": transformRequestStub
});
generate.generateReport(mReq, mRes);
convertStub.yield(fakeError, null);
sinon.assert.calledWith(transformRequestStub);
sinon.assert.calledWith(convertStub, {}, sinon.match.func);
sinon.assert.calledWith(errorHandlerStub, fakeError);
sinon.assert.calledWith(logSpy.firstCall, "HELLO");
sinon.assert.calledWith(logSpy.secondCall, "BYE");
});
});
});
Unit test result with coverage report:
report-exporter
generateReport
HELLO
BYE
✓ Should return an error response (94ms)
1 passing (98ms)
----------------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------------------|----------|----------|----------|----------|-------------------|
All files | 87.5 | 50 | 62.5 | 87.5 | |
convert.js | 50 | 100 | 0 | 50 | 2 |
error-handler.js | 50 | 100 | 0 | 50 | 2 |
index.js | 84.62 | 50 | 100 | 84.62 | 14,15 |
index.spec.js | 100 | 100 | 100 | 100 | |
request-converter.js | 50 | 100 | 0 | 50 | 2 |
----------------------|----------|----------|----------|----------|-------------------|
Source code: https://github.com/mrdulin/mocha-chai-sinon-codelab/tree/master/src/stackoverflow/47352972

Resources