模拟泵 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
我正在尝试使用 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