Last active
December 26, 2017 13:20
-
-
Save skyjur/6e9d8cff8957acb30fe0509863eeb6f5 to your computer and use it in GitHub Desktop.
Record/Replay api calls
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| }); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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