简体   繁体   中英

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM