Search code examples
javascripttypescriptmocha.jsacceptance-testingnock

How to use nock to record request and responses to files and use it to playback in mocha acceptance test?


I inherited a typescript@2 project that has no tests in place.

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.

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?

I've looked into nock as it appears to provide this functionality, yet how do I use it?

(I don't provide an example as I intend to answer my question myself as I just recently have been through the entire ordeal.)


Solution

  • 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:

    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).

    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:

    export type MockeMode = 'recording'|'playback'|'none';
    

    which I then can inject into my appRunner function:

    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:

    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.

    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.

    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.