模拟泵 node_module 使用 jest - Typescript 的不同实现

Mocking pump node_module with different implementations using jest - Typescript

我正在尝试使用 nodejs 实现 gcloud-storage 并使用 typescript 测试它们 这是我的实际 class

请暂时不要考虑日志记录实现。 存储通过外部服务调用进行身份验证 -

const str = GcloudAuthenticationInstance.createGcloudAuthenticationBucket();

并且我愿意存储在 gcloud 中的文件是使用 streams 和 pump 模块进行操作的

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 });
                });
        });
    };

现在有流完成或出错的可能性,我想测试两者。 我可以像这样在任何测试套件声明之前将泵 npm 模块作为一个整体模拟在顶部 jest.mock。

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);
    })
);

所以上面是工作场景的实现,我将现有文件通过管道传输到输出流并返回流,使泵的模拟工作。这是我的测试规范文件

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();
    });

});

现在这适用于模拟泵调用。但我想在同一规范中测试流发出错误的场景。是否有可能在另一个测试规范中覆盖 mockImplementation。由于这是一个 npm 模块,我在顶部编写了 jest.mock() ,它将作为整个测试套件的模拟,但不确定如何覆盖它。过去 3 天我一直在尝试,但无法弄清楚。有什么办法可以实现吗?

这是使用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
  });
});

带有覆盖率报告的单元测试结果:

 PASS  Whosebug/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

源代码:https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/Whosebug/61031410