Skip to content

Instantly share code, notes, and snippets.

@skyjur
Last active December 26, 2017 13:20
Show Gist options
  • Select an option

  • Save skyjur/6e9d8cff8957acb30fe0509863eeb6f5 to your computer and use it in GitHub Desktop.

Select an option

Save skyjur/6e9d8cff8957acb30fe0509863eeb6f5 to your computer and use it in GitHub Desktop.
Record/Replay api calls
class Bookshop {
api = BookshopTransportApi;
async findBookByName(name: string) {
val result = await this.api.call({
collection: 'books',
query: {name: id}
});
// process json result, return Book instance
}
}
class BookshopTransportApi {
async call(requestBody) {
let resp = await fetch('http://example.com/bookshop/api', {
body: JSON.stringify(requestBody);
});
return await resp.json();
}
}
import {env} from 'process';
import * as sinon from 'sinon';
import {recordCalls, replayCalls} from './rec';
import {Bookshop} from './api';
const recordReplay = env.MODE == 'record' ? recordCalls : replayCalls;
describe('Bookshop', () => {
var sandbox;
var bookshop;
beforeAll(() => {
sandbox = sinon.sandbox();
bookshop = new Bookshop;
var fake = recordReplay(bookshop.api.call);
sinon.stub(bookshop.api, 'call').callsFake(fake);
}
it('Should return a book', () => {
let book = await bookshop.findBookByName('...');
// do assertion on book
});
});
import * as fs from "fs";
interface Config {
name: string;
recFile: string;
key: (args: any) => string;
toJSON: (data: any) => any;
fromJSON: (data: string) => any;
}
interface Store {
[key: string]: {
isPromise: boolean,
result: any
}
}
export function recordCalls<T extends Function>(target: T, config?: Partial<Config>) : T {
let conf = getConfig(target, config);
return <any>function(this: any) {
let args = Array.from(arguments);
let result = target.apply(this, args);
if(result['then']) {
return result.then((result: any) => {
recordCall(conf, args, result, true);
return result;
})
} else {
recordCall(conf, args, result);
return result;
}
}
}
export function replayCalls<T extends Function>(target: T, config?: Partial<Config>) : T {
let conf = getConfig(target, config);
return <any>function() {
let args = Array.from(arguments);
return replayCall(conf, args);
}
}
function getConfig<T extends Function>(target: T, config?: Partial<Config>) : Config {
config = config || {};
let name = config.name || target.name;
return {
name,
recFile: getCallerBasename().replace(/[.][a-z]+$/, '') + '.rec.json',
key: config.key ? config.key : keyFunc(name),
toJSON: config.toJSON || (val => val),
fromJSON: config.fromJSON || (val => val)
}
}
function keyFunc(targetName: string) {
return function(args: any[]) : string {
let serializedArgs = args.map(val => JSON.stringify(val)).join(', ');
return `${targetName}(${ serializedArgs })`;
}
}
function getCallerBasename() : string {
var origPrepareStackTrace = (<any>Error).prepareStackTrace;
// override because stack formatting can be incosistent
(<any>Error).prepareStackTrace = function (err: any, stack: any) {
return stack
};
var err = new Error();
let stack : any = err.stack;
(<any>Error).prepareStackTrace = origPrepareStackTrace;
return stack[3].getFileName();
}
function recordCall(config: Config, args: any, result: any, promise=false) {
let key = config.key(args);
let resultSerialized = config.toJSON(result);
let data : Store = {};
try {
data = JSON.parse(fs.readFileSync(config.recFile).toString());
} catch(e){
}
data[key] = {
isPromise: promise,
result: resultSerialized
};
fs.writeFileSync(config.recFile, JSON.stringify(data, undefined, 4));
}
function replayCall(config: Config, args: any[]) {
let key = config.key(args);
let data : Store = {};
try {
data = JSON.parse(fs.readFileSync(config.recFile).toString());
} catch {}
if(!data[key]) {
throw new Error(`Call ${key} was not recorded in ${config.recFile}`);
}
let result = config.fromJSON(data[key].result);
return data[key].isPromise ? Promise.resolve(result) : result;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment