I am using JEST for testing a package I am creating and I am using 100% coverage in all aspects.
On windows everything works fine, coverage is 100% as expected but when I run it on a docker even though the node version is the same (14.18.1), jest fails to detect 2 lines of code that I'm am sure that they were reached, here is the code:
import callIf from 'src/tools/call-if';
import zlib from 'zlib';
export default class CompressorEngine {
static preparaData<T>(data: T): Buffer {
const isNullOrUndefined = [null, undefined].includes(data as any);
if (isNullOrUndefined) {
return Buffer.from('null', 'utf-8');
}
return Buffer.from(JSON.stringify(data), 'utf-8');
}
static compress(uncompressedData: Buffer, compressionLevel: number = 9) {
return new Promise<Buffer>((resolve, reject) => {
zlib.deflate(uncompressedData, { level: compressionLevel }, (error, result) => {
callIf(error, reject, error);
callIf(!error, resolve, result);
});
});
}
static decompress(compressedData: Buffer, compressionLevel: number = 9) {
return new Promise<Buffer>((resolve, reject) => {
zlib.inflate(compressedData, { level: compressionLevel }, (error, result) => {
callIf(error, reject, error);
callIf(!error, resolve, result);
});
});
}
}
the src/tools/call-if code:
const callIf = (expression: any, callback: Function, ...args: any[]) => {
if (expression) return callback(...args);
};
export default callIf;
and here are my tests for this class:
import CompressorEngine from 'src/core/compressor';
describe('CompressorEngine', () => {
it('should compress and decompress data', async () => {
const input = 'test';
const compressed = await CompressorEngine.compress(CompressorEngine.preparaData(input));
const uncompressed = await CompressorEngine.decompress(compressed);
expect(uncompressed.toString('utf-8')).toEqual(`"${input}"`);
});
it('should compress and decompress null as "null"', async () => {
const input = null;
const compressed = await CompressorEngine.compress(CompressorEngine.preparaData(input));
const uncompressed = await CompressorEngine.decompress(compressed);
expect(JSON.parse(uncompressed.toString('utf-8'))).toEqual(null);
});
describe('prepareData', () => {
it('should return a buffer for null value as "null"', () => {
Array.prototype.includes = jest.fn(Array.prototype.includes);
const buffer = CompressorEngine.preparaData(null);
expect(buffer.toString('utf-8')).toBe('null');
expect(Array.prototype.includes).toHaveBeenCalled();
expect(Array.prototype.includes).toHaveReturnedWith(true);
});
it('should return a buffer for undefined value as "null"', () => {
const buffer = CompressorEngine.preparaData(undefined);
expect(buffer.toString('utf-8')).toBe('null');
});
});
});
The problem is on prepareData it says as if the if(true) is not reached, but it is as the first prepareData specific test describes "expect(Array.prototype.includes).toHaveReturnedWith(true);" but the generated coverage says it wasn't covered, how come?
Related
We have module within there is some initial logic which test that some value was configured, if not, it throws error.. then also the module provides methods. I want to describe this in specification (test), using Jest framework and test the feature. Here is simplified reproduced example:
// dependency.service.ts
export const something = {
method() {
return "methodValue";
}
};
export default function somethingElse() {
return "somethingElseValue";
}
// index.ts
import somethingElse, { something } from "./dependency.service";
const value1 = somethingElse();
const value2 = something.method();
console.log("ROOT somethingElse", value1);
console.log("ROOT something.method", value2);
// initialisation of module fails
if(value1 === 'throw_error' || value2 === 'throw_error') {
throw 'Some error';
}
export function smElse() {
const value = somethingElse();
console.log("somethingElse", value);
return value;
}
export function smMethod() {
const value = something.method();
console.log("somethingElse", value);
return value;
}
// index.spec.ts
import { smElse, smMethod } from './index';
jest.mock('./dependency.service', () => ({
__esModule: true,
default: jest.fn(() => 'MOCKED_somethingElseValue'),
something: {
method: jest.fn(() => 'MOCKED_methodValue'),
},
}));
describe('index', () => {
// some tests for happy paths
it('smElse returns mocked value', () => {
expect(smElse()).toMatchInlineSnapshot(`"MOCKED_somethingElseValue"`);
});
it('smMethod returns mocked value', () => {
expect(smMethod()).toMatchInlineSnapshot(`"MOCKED_methodValue"`);
});
it('smElse returns per test mocked value', () => {
const somethingElseMocked = require('./dependency.service').default;
somethingElseMocked.mockReturnValueOnce('ANOTHER_MOCKED_somethingElseValue');
expect(smElse()).toMatchInlineSnapshot(`"ANOTHER_MOCKED_somethingElseValue"`);
});
it('smMethod returns per test mocked value', () => {
const something = require('./dependency.service').something;
something.method.mockReturnValueOnce('ANOTHER_MOCKED_methodValue');
expect(smMethod()).toMatchInlineSnapshot(`"ANOTHER_MOCKED_methodValue"`);
});
// this is testing the throwing error in module root
it('throws error when somethingElse returns specific message', () => {
expect.assertions(1);
jest.isolateModules(() => {
const somethingElseMocked = require('./dependency.service').default;
somethingElseMocked.mockReturnValueOnce('throw_error');
try {
require('./index');
} catch (error) {
expect(error).toBe("Some error");
}
});
});
it('throws error when something.method returns specific message', () => {
expect.assertions(1);
jest.isolateModules(() => {
const somethingMethodMocked = require('./dependency.service').something.method;
somethingMethodMocked.mockReturnValueOnce('throw_error');
try {
require('./index');
} catch (error) {
expect(error).toBe("Some error");
}
});
});
});
"Try catch in isolation" solution does not work with async code as isolateModules method does not support async functions yet, reference: https://github.com/facebook/jest/issues/10428. I need alternative solution which would support async code.
Whole reproduced example repo here: https://github.com/luckylooke/jestTestModuleRootThrow/tree/main
EDIT:
I found out that some asynchronicity is supported by isolateModules, at least my use case, once I used expect.assertions(1) following test works as expected:
it('throws error when token data are not valid', async () => {
expect.assertions(1);
jest.isolateModules(async () => {
require('crypto-package').someMethod.mockReturnValueOnce(Promise.resolve({
decoderMethod: () => Promise.resolve('{ broken data }'),
}));
const { getTokenData } = require("./decoder.service");
await expect(getTokenData('34534xxxxxxxxxxxxxxx12628')).rejects.toMatchInlineSnapshot(`"Invalid token data"`);
});
});
`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()
})
I need to test 'readline.createInterface'.
Below is the code that I need to test:
private createReadStreamSafe(filePath: string): Promise<fs.ReadStream> {
return new Promise((resolve, reject) => {
const fileStream = fs.createReadStream(filePath)
console.log('file Stream')
fileStream
.on('error', () => {
reject('create read stream error')
})
.on('open', () => {
resolve(fileStream)
})
})
}
async start() {
const fileStream = await this.createReadStreamSafe(this.filePath)
const rl = readline.createInterface({
input: fileStream,
output: process.stdout,
terminal: false
})
for await (const line of rl) {
...
}
}
I tried following code:
it('should work', async () => {
const mockedReadStream = new Readable()
jest.spyOn(fs, 'createReadStream').mockReturnValue(mockedReadStream as any)
jest.spyOn(readline, 'createInterface').mockImplementation(() => {
const lines = ['text', 'text2', 'text3']
return {
[Symbol.asyncIterator]() {
return {
i: 0,
next: () => {
if (this.i < 3) {
return Promise.resolve({ value: lines[this.i++], done: false })
}
return Promise.resolve({ done: true })
}
}
}
} as any
})
const app = new App('myFile.txt')
let promise = app.start()
mockedReadStream.emit('open')
await expect(promise).resolves.toBe(undefined)
})
But following code is never reached
for await (const line of rl) {
...
}
Is there a way to mock readline.createInterface and then it works with the for await (const line of rl)?
The issue is: the async iterable object is not triggered during the test.
Solution, we can just use an array in the mock, like this:
jest.spyOn(readline, 'createInterface').mockImplementationOnce(() => {
return ['text1', 'text2'] as any
})
Since for await (const item of iterable) works for async iterable objects as well sync iterables. With sync iterables, they will be executed automatically.
I have been trying to mock the #google-cloud/storage for my implementation so that I could test it without having to hit the cloud-storge in gcp and so far it has all been in vain
I have tried to mock the node_module scope folder using the jest doc and that didnt work out
Hence I tried using below
This is my implementation class
import { GcloudAuthenticationInstance } from '../common/services/gcloud.authentication';
import * as fs from 'fs';
import pump from 'pump';
import pino from 'pino';
import * as _ from 'lodash';
import {
ENV_NAME_DEV,
GCLOUD_DATABASE_BUCKET_DEV,
GCLOUD_DATABASE_BUCKET_PROD,
GCLOUD_ENV_STR_BUCKET_NAME,
GCLOUD_STORED_FILE_NAME_DEV,
GCLOUD_STORED_FILE_NAME_PROD,
GCLOUD_UPLOAD_FILE_DEV_LOCAL_PATH,
GCLOUD_UPLOAD_FILE_PROD_LOCAL_PATH,
} from '../common/util/app.constants';
import { PinoLoggerServiceInstance } from '../common/services/pino.logger.service';
import { AppUtilServiceInstance } from '../common/services/app.util.service';
export const uploadEnvFiles = async (env_name: string) => {
const LOGGER: pino.Logger = PinoLoggerServiceInstance.getLogger(__filename);
return new Promise(async (res, rej) => {
// This just returns the Storage() instance with keyFileName and projectID
//of google cloud console being set so authentication takes place
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 });
});
});
};
export const downloadEnvFiles = async (env_name): Promise<any> => {
const LOGGER: pino.Logger = PinoLoggerServiceInstance.getLogger(__filename);
return new Promise(async (res, rej) => {
const str = GcloudAuthenticationInstance.createGcloudAuthenticationBucket();
try {
const [files] = await str.bucket(GCLOUD_ENV_STR_BUCKET_NAME).getFiles();
const filteredFile =
ENV_NAME_DEV === env_name
? _.find(files, (file) => {
c
return file.name.includes(GCLOUD_STORED_FILE_NAME_DEV);
})
: _.find(files, (file) => {
return file.name.includes(GCLOUD_STORED_FILE_NAME_PROD);
});
res({
status: 'Success',
code: 200,
error: null,
stream: str
.bucket(GCLOUD_ENV_STR_BUCKET_NAME)
.file(filteredFile.name)
.createReadStream()
});
} catch (err) {
LOGGER.error('Error in retrieving files from gcloud:'+err);
rej({ status: 'Error', error: err, code: 500 });
}
});
};
This is my jest ts
bucket.operations.int.spec.ts
I've tried to include the mock inline
import { GcloudAuthenticationInstance } from '../common/services/gcloud.authentication';
const { Storage } = require('#google-cloud/storage');
const { Bucket } = require('#google-cloud/storage');
import { File } from '#google-cloud/storage';
import { mocked } from 'ts-jest/utils'
const fs = require('fs');
import * as path from 'path';
import pump from 'pump';
import * as BucketOperations from './bucket.operations';
import { GCLOUD_ENV_STR_BUCKET_NAME } from '../common/util/app.constants';
const { PassThrough } = require('stream');
const fsMock = jest.mock('fs');
// Here we are trying to mock pump with a function returned
// since pump () is the actual fucntion, we are mocking the function to return a value
// which is just a value of "on" eventlistener.. so we indicate that this will be substituted
// with another mocked function
jest.genMockFromModule('#google-cloud/storage');
jest.mock('#google-cloud/storage', () => {
const mockedFile = jest.fn().mockImplementation(() => {
return {
File: jest.fn().mockImplementation(() => {
return {
name: 'dev.txt',
createReadStream: jest
.fn()
.mockReturnValue(
fs.createReadStream(
path.resolve(process.cwd(), './tests/cloud-storage/sample-read.txt')
)
),
createWriteStream: jest
.fn()
.mockReturnValue(
fs.createWriteStream(
path.resolve(process.cwd(), './tests/cloud-storage/sample-write.txt')
)
)
};
})
};
});
const mockedBUcket = jest.fn().mockImplementation(() => {
return {
Bucket: jest.fn().mockImplementation(() => {
return {
constructor: jest.fn().mockReturnValue('test-bucket'),
getFiles: jest.fn().mockReturnValue([mockedFile])
}
})
}
});
return {
Storage: jest.fn().mockImplementation(() => {
return {
constructor: jest.fn().mockReturnValue('test-storage'),
bucket: mockedBUcket,
file: mockedFile,
createWriteStream: jest.fn().mockImplementation(() =>
fs.createWriteStream(path.resolve(process.cwd(), './tests/cloud-storage/sample-write.txt')))
};
})
};
});
jest.mock('pump', () => {
const mPump = { on: jest.fn() };
return jest.fn(() => mPump);
});
describe('Test suite for testing bucket operations', () => {
const mockedStorage = mocked(Storage, true);
const mockeddFile = mocked(File, true);
const mockeddBucket = mocked(Bucket, true);
function cancelCloudStorageMock() {
//mockCloudStorage.unmock('#google-cloud/storage');
mockedStorage.mockClear();
mockeddBucket.mockClear();
mockeddFile.mockClear();
jest.unmock('#google-cloud/storage');
jest.requireActual('#google-cloud/storage');
}
function cancelFsMock() {
jest.unmock('fs');
jest.requireActual('fs');
}
afterEach(() => {
jest.clearAllMocks();
//jest.restoreAllMocks();
});
test('test for uploadfiles - success', async (done) => {
cancelFsMock();
pump().on = jest.fn(function(this: any, event, callback) {
if (event === 'finish') {
callback();
}
return this;
});
const actual = await BucketOperations.uploadEnvFiles('dev');
expect(actual).toEqual(
expect.objectContaining({
status: 'Success',
code: 201,
})
);
done();
});
test('test downloadEnvFiles - success', async (done) => {
jest.unmock('fs');
const fMock = (File.prototype.constructor = jest.fn().mockImplementation(() => {
return {
storage: new Storage(),
bucket: 'testBucket',
acl: 'test-acl',
name: 'dev.txt',
parent: 'parent bucket',
};
}));
const bucketGetFilMock = (Bucket.prototype.getFiles = jest.fn().mockImplementation(() => {
return [fMock];
}));
// Get files should be an array of File from google-cloud-storage
//Bucket.prototype.getFiles = jest.fn().mockReturnValue([mockedFsConstructor]);
//Storage.prototype.bucket = jest.fn().mockReturnValue(new Storage());
const mockReadable = new PassThrough();
const mockWritable = new PassThrough();
jest.spyOn(fs, 'createReadStream').mockReturnValue(
fs.createWriteStream(path.resolve(process.cwd(), './tests/cloud-storage/sample-read.txt'))
);
await BucketOperations.downloadEnvFiles('dev');
done();
});
});
This is the exception I end up with. Upon debugging I see that the mocked instances are trying to execute, but it doesn't execute the file method in Storage mock. This is not available in #google-cloud/storage but I did try to mock it. Is there a way to mock just the usage of google-cloud/storage using jest?
EDIT:
Here is the exception:
TypeError: str.bucket(...).file is not a function
at /home/vijaykumar/Documents/Code/Nestjs/cloud-storage-app/src/gcloud/bucket.operations.ts:37:6
at Generator.next (<anonymous>)
at /home/vijaykumar/Documents/Code/Nestjs/cloud-storage-app/src/gcloud/bucket.operations.ts:8:71
at new Promise (<anonymous>)
at Object.<anonymous>.__awaiter (/home/vijaykumar/Documents/Code/Nestjs/cloud-storage-app/src/gcloud/bucket.operations.ts:4:12)
at /home/vijaykumar/Documents/Code/Nestjs/cloud-storage-app/src/gcloud/bucket.operations.ts:22:40
at new Promise (<anonymous>)
at /home/vijaykumar/Documents/Code/Nestjs/cloud-storage-app/src/gcloud/bucket.operations.ts:22:9
Thanks to #ralemos. I was able to find the answer on how I mocked
Here is the complete implementation.
I've added a few more test stories as well
So jest.mock() esp the #google-cloud/storage modules, needs to be mocked in a different way. The Bucket of the Storage has all the details of the files in gcp storage, so that needs to be mocked first, I also mocked the File (this is of type #google-cloud/storage). Now I added the mockedFile to the mockedBucket and from there to the mockedStorage. I've also added all the methods and properties and implemented a mock for all of them.
There is a lodash node_module usage in my test file, so I mocked that implementation as well. Now everything works fine.
import { GcloudAuthenticationInstance } from '../common/services/gcloud.authentication';
const { Storage } = require('#google-cloud/storage');
const fs = require('fs');
import * as path from 'path';
import pump from 'pump';
import * as BucketOperations from './bucket.operations';
const { PassThrough } = require('stream');
const fsMock = jest.mock('fs');
const mockedFile = {
name: 'dev.txt',
createWriteStream: jest.fn().mockImplementation(() => {
return fs.createWriteStream(path.resolve(process.cwd(), './tests/cloud-storage/sample-write.txt'));
}),
createReadStream: jest.fn().mockImplementation(() => {
return fs.createReadStream(path.resolve(process.cwd(), './tests/cloud-storage/sample-read.txt'));
}),
};
jest.mock('lodash', () => {
return {
find: jest.fn().mockImplementation(() => {
return mockedFile;
})
};
});
const mockedBucket = {
file: jest.fn(() => mockedFile),
getFiles: jest.fn().mockImplementation(() => {
const fileArray = new Array();
fileArray.push(mockedFile);
return fileArray;
})
};
const mockedStorage = {
bucket: jest.fn(() => mockedBucket)
};
jest.mock('#google-cloud/storage', () => {
return {
Storage: jest.fn(() => mockedStorage)
};
});
jest.mock('pump', () => {
const mPump = { on: jest.fn() };
return jest.fn(() => mPump);
});
describe('Test suite for testing bucket operations', () => {
function cancelCloudStorageMock() {
jest.unmock('#google-cloud/storage');
jest.requireActual('#google-cloud/storage');
}
function cancelFsMock() {
jest.unmock('fs');
jest.requireActual('fs');
}
afterEach(() => {
jest.clearAllMocks();
//jest.restoreAllMocks();
});
test('test for uploadfiles - success', async (done) => {
pump().on = jest.fn(function(this: any, event, callback) {
if (event === 'finish') {
callback();
}
return this;
});
const actual = await BucketOperations.uploadEnvFiles('dev');
expect(actual).toEqual(
expect.objectContaining({
status: 'Success',
code: 201,
})
);
done();
});
test('test downloadEnvFiles - success', async (done) => {
jest.unmock('fs');
const downloadRes = await BucketOperations.downloadEnvFiles('dev');
expect(downloadRes).toBeDefined();
expect(downloadRes).toEqual(expect.objectContaining({code:200, status: 'Success'}));
done();
});
test('test for uploadfiles- failure', async (done) => {
cancelCloudStorageMock();
const bucketStorageSpy = jest
.spyOn(GcloudAuthenticationInstance, 'createGcloudAuthenticationBucket')
.mockImplementation(() => {
return new Storage({
projectId: 'testId',
keyFilename: path.resolve(process.cwd(), './tests/cloud-storage/sample-read.txt'),
scopes: ['testScope'],
autoRetry: false,
});
});
const mockReadable = new PassThrough();
const mockWritable = new PassThrough();
fs.createWriteStream = jest.fn().mockReturnValue(mockWritable);
fs.createReadStream = jest.fn().mockReturnValue(mockReadable);
pump().on = jest.fn(function(this: any, event, callback) {
if (event === 'error') {
callback();
}
return this;
});
const actual = BucketOperations.uploadEnvFiles('prod');
expect(actual).rejects.toEqual(
expect.objectContaining({
status: 'Error',
code: 500,
})
);
expect(bucketStorageSpy).toHaveBeenCalledTimes(1);
done();
});
test('test download - make the actual call - rej with auth error', async (done) => {
cancelCloudStorageMock();
console.dir(Storage);
const mockReadable = new PassThrough();
const mockWritable = new PassThrough();
fs.createWriteStream = jest.fn().mockReturnValue(mockWritable);
fs.createReadStream = jest.fn().mockReturnValue(mockReadable);
const createGcloudAuthenticationBucketSpy = jest
.spyOn(GcloudAuthenticationInstance, 'createGcloudAuthenticationBucket')
.mockImplementation(() => {
return new Storage();
});
try {
await BucketOperations.downloadEnvFiles('dev');
} catch (err) {
expect(err.code).toBe(500);
expect(err.status).toBe('Error');
}
expect(createGcloudAuthenticationBucketSpy).toHaveBeenCalledTimes(1);
createGcloudAuthenticationBucketSpy.mockReset();
done();
});
});
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