I inherited a typescript@2 project that has no tests in place.
我继承了一个没有测试的typescript @ 2项目。
It's basically a cli task runner, and a task requests an external api multiple time in order to create a file. As a a first failsafe, I want to set up acceptance tests.
它基本上是一个cli任务运行器,并且一个任务多次请求外部api以创建文件。作为第一个故障保护,我想建立验收测试。
Therefore, I want to mock the calls to the external api and to fetch the response from a local file. How do I achieve that?
因此,我想模拟对外部api的调用并从本地文件中获取响应。我如何实现这一目标?
I've looked into nock as it appears to provide this functionality, yet how do I use it?
我已经看了nock,因为它似乎提供了这个功能,但我该如何使用它?
(I don't provide an example as I intend to answer my question myself as I just recently have been through the entire ordeal.)
(我没有提供一个例子,因为我打算自己回答我的问题,因为我刚刚经历了整个考验。)
1 个解决方案
#1
2
I have refactored my application that all the call to the to the external api happen when a Task object executes its execute
method. Such a task implements the interface ITask
:
我重构了我的应用程序,当一个Task对象执行其execute方法时,所有对外部api的调用都会发生。这样的任务实现了ITask接口:
import {ReadStream} from 'fs';
export interface ITask {
execute(): Promise<ReadStream>;
}
This allowed me to wrap a Task inside either a recorder or playback decorator. (I also don't let the execute
create a file anymore but it returns the Promise of a Stream. In my normal workflow I would dump that stream to the file ystem (or upload it where ever I wanted).
这允许我将一个Task包装在录音机或播放装饰器中。 (我也不让执行创建一个文件,但它返回一个Stream的Promise。在我的正常工作流程中,我会将该流转储到文件系统(或者将其上传到我想要的地方)。
RecordDecorator:
import {writeFile} from 'fs';
import {ITask} from './ITask';
import nock = require('nock');
import mkdirp = require('mkdirp');
import {ReadStream} from 'fs';
export class TaskMockRecorder implements ITask {
constructor(private task: ITask, private pathToFile: string) {
}
public async execute(): Promise <ReadStream> {
this.setupNock();
const stream = await this.task.execute();
this.writeRecordFile();
return Promise.resolve(stream);
}
private writeRecordFile() {
const nockCallObjects = nock.recorder.play();
mkdirp(this.pathToFile, async() => {
writeFile(`${this.pathToFile}`, JSON.stringify(nockCallObjects, null, 4));
});
}
private setupNock() {
nock.recorder.rec({
dont_print: true,
enable_reqheaders_recording: true,
output_objects: true,
});
}
}
PlayBackDecorator
import {ITask} from './ITask';
import {ReadStream} from 'fs';
import {Partner} from '../Types';
import nock = require('nock');
export class TaskMockPlaybackDecorator implements ITask {
constructor(private task: ITask, private pathToFile: string) {
}
public async execute(): Promise<ReadStream> {
nock.load(this.pathToFile);
nock.recorder.play();
return this.task.execute();
}
}
Decorating the task
I furthermore introduced the custom type MockMode
:
我进一步介绍了自定义类型MockMode:
export type MockeMode = 'recording'|'playback'|'none';
which I then can inject into my appRunner function:
然后我可以注入我的appRunner函数:
export async function appRun(config: IConfig, mockMode: MockeMode): Promise<ReadStream> {
let task: ITask;
task = new MyTask(config);
const pathToFile = `tapes/${config.taskName}/tape.json`;
switch (mockMode) {
case 'playback':
console.warn('playback mode!');
task = new TaskMockPlaybackDecorator(task, path);
break;
case 'recording':
console.warn('recording mode!');
task = new TaskMockRecorder(task, path);
break;
default:
console.log('normal mode');
}
const csvStream = await task.execute();
return Promise.resolve(csvStream);
}
Implementing the acceptance test:
I now had to add reference files and set up the mocha test that compares both the generated stream from a playback run with the reference file:
我现在必须添加参考文件并设置mocha测试,将来自回放运行的生成流与参考文件进行比较:
import nock = require('nock');
import {appRun} from '../../src/core/task/taskRunner';
import {createReadStream} from 'fs';
import {brands} from '../../src/config/BrandConfig';
import isEqual = require('lodash/isEqual');
const streamEqual = require('stream-equal');
describe('myTask', () => {
const myConfig = { // myConfig // }
const referencePath = `references/${myConfig.taskName}.csv`;
it(`generates csv that matches ${referencePath}`, async() => {
nock.load(`tapes/${config}.taskName}/tape.json`);
nock.recorder.play();
return new Promise(async(resolve, reject) => {
const actual = await appRun(myConfig, 'playback');
const expected = createReadStream(referencePath);
streamEqual(actual, expected, (err: any, isEqual: boolean) => {
if (err) {
reject(err);
}
if (isEqual) {
resolve('equals');
return;
}
reject('not equals');
});
});
});
});
Depending on the size of the taped json request/respones one might need to increase the run size via timeout
, as the default is 2 seconds and these kind of test might run slower.
根据录制的json请求/响应的大小,可能需要通过超时增加运行大小,因为默认值为2秒,并且这些类型的测试可能运行得更慢。
mocha --recursive dist/tests -t 10000
This approach also makes it possible to easily update the tapes, one can just pass the mockMode
parameter from as an argument and it will update the tape.json
.
这种方法也可以轻松更新磁带,只需将mockMode参数作为参数传递,它就会更新tape.json。
Downside is that the tape.json
might be huge depending on the amount of traffic, yet this was intentional as as a first step I wanted to be sure that my application behaves the same on any changes to its codebase.
缺点是tape.json可能是巨大的,具体取决于流量,但这是故意的,因为我希望确保我的应用程序在其代码库的任何更改上的行为相同。
#1
2
I have refactored my application that all the call to the to the external api happen when a Task object executes its execute
method. Such a task implements the interface ITask
:
我重构了我的应用程序,当一个Task对象执行其execute方法时,所有对外部api的调用都会发生。这样的任务实现了ITask接口:
import {ReadStream} from 'fs';
export interface ITask {
execute(): Promise<ReadStream>;
}
This allowed me to wrap a Task inside either a recorder or playback decorator. (I also don't let the execute
create a file anymore but it returns the Promise of a Stream. In my normal workflow I would dump that stream to the file ystem (or upload it where ever I wanted).
这允许我将一个Task包装在录音机或播放装饰器中。 (我也不让执行创建一个文件,但它返回一个Stream的Promise。在我的正常工作流程中,我会将该流转储到文件系统(或者将其上传到我想要的地方)。
RecordDecorator:
import {writeFile} from 'fs';
import {ITask} from './ITask';
import nock = require('nock');
import mkdirp = require('mkdirp');
import {ReadStream} from 'fs';
export class TaskMockRecorder implements ITask {
constructor(private task: ITask, private pathToFile: string) {
}
public async execute(): Promise <ReadStream> {
this.setupNock();
const stream = await this.task.execute();
this.writeRecordFile();
return Promise.resolve(stream);
}
private writeRecordFile() {
const nockCallObjects = nock.recorder.play();
mkdirp(this.pathToFile, async() => {
writeFile(`${this.pathToFile}`, JSON.stringify(nockCallObjects, null, 4));
});
}
private setupNock() {
nock.recorder.rec({
dont_print: true,
enable_reqheaders_recording: true,
output_objects: true,
});
}
}
PlayBackDecorator
import {ITask} from './ITask';
import {ReadStream} from 'fs';
import {Partner} from '../Types';
import nock = require('nock');
export class TaskMockPlaybackDecorator implements ITask {
constructor(private task: ITask, private pathToFile: string) {
}
public async execute(): Promise<ReadStream> {
nock.load(this.pathToFile);
nock.recorder.play();
return this.task.execute();
}
}
Decorating the task
I furthermore introduced the custom type MockMode
:
我进一步介绍了自定义类型MockMode:
export type MockeMode = 'recording'|'playback'|'none';
which I then can inject into my appRunner function:
然后我可以注入我的appRunner函数:
export async function appRun(config: IConfig, mockMode: MockeMode): Promise<ReadStream> {
let task: ITask;
task = new MyTask(config);
const pathToFile = `tapes/${config.taskName}/tape.json`;
switch (mockMode) {
case 'playback':
console.warn('playback mode!');
task = new TaskMockPlaybackDecorator(task, path);
break;
case 'recording':
console.warn('recording mode!');
task = new TaskMockRecorder(task, path);
break;
default:
console.log('normal mode');
}
const csvStream = await task.execute();
return Promise.resolve(csvStream);
}
Implementing the acceptance test:
I now had to add reference files and set up the mocha test that compares both the generated stream from a playback run with the reference file:
我现在必须添加参考文件并设置mocha测试,将来自回放运行的生成流与参考文件进行比较:
import nock = require('nock');
import {appRun} from '../../src/core/task/taskRunner';
import {createReadStream} from 'fs';
import {brands} from '../../src/config/BrandConfig';
import isEqual = require('lodash/isEqual');
const streamEqual = require('stream-equal');
describe('myTask', () => {
const myConfig = { // myConfig // }
const referencePath = `references/${myConfig.taskName}.csv`;
it(`generates csv that matches ${referencePath}`, async() => {
nock.load(`tapes/${config}.taskName}/tape.json`);
nock.recorder.play();
return new Promise(async(resolve, reject) => {
const actual = await appRun(myConfig, 'playback');
const expected = createReadStream(referencePath);
streamEqual(actual, expected, (err: any, isEqual: boolean) => {
if (err) {
reject(err);
}
if (isEqual) {
resolve('equals');
return;
}
reject('not equals');
});
});
});
});
Depending on the size of the taped json request/respones one might need to increase the run size via timeout
, as the default is 2 seconds and these kind of test might run slower.
根据录制的json请求/响应的大小,可能需要通过超时增加运行大小,因为默认值为2秒,并且这些类型的测试可能运行得更慢。
mocha --recursive dist/tests -t 10000
This approach also makes it possible to easily update the tapes, one can just pass the mockMode
parameter from as an argument and it will update the tape.json
.
这种方法也可以轻松更新磁带,只需将mockMode参数作为参数传递,它就会更新tape.json。
Downside is that the tape.json
might be huge depending on the amount of traffic, yet this was intentional as as a first step I wanted to be sure that my application behaves the same on any changes to its codebase.
缺点是tape.json可能是巨大的,具体取决于流量,但这是故意的,因为我希望确保我的应用程序在其代码库的任何更改上的行为相同。