import * as chai from 'chai'; import * as _ from 'lodash'; import { chaiSetup } from './chai_setup'; chaiSetup.configure(); const expect = chai.expect; export async function testWithReferenceFuncAsync( referenceFunc: (p0: P0) => Promise, testFunc: (p0: P0) => Promise, values: [P0], ): Promise; export async function testWithReferenceFuncAsync( referenceFunc: (p0: P0, p1: P1) => Promise, testFunc: (p0: P0, p1: P1) => Promise, values: [P0, P1], ): Promise; export async function testWithReferenceFuncAsync( referenceFunc: (p0: P0, p1: P1, p2: P2) => Promise, testFunc: (p0: P0, p1: P1, p2: P2) => Promise, values: [P0, P1, P2], ): Promise; export async function testWithReferenceFuncAsync( referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise, testFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise, values: [P0, P1, P2, P3], ): Promise; export async function testWithReferenceFuncAsync( referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise, testFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise, values: [P0, P1, P2, P3, P4], ): Promise; /** * Tests the behavior of a test function by comparing it to the expected * behavior (defined by a reference function). * * First the reference function will be called to obtain an "expected result", * or if the reference function throws/rejects, an "expected error". Next, the * test function will be called to obtain an "actual result", or if the test * function throws/rejects, an "actual error". The test passes if at least one * of the following conditions is met: * * 1) Neither the reference function or the test function throw and the * "expected result" equals the "actual result". * * 2) Both the reference function and the test function throw and the "actual * error" message *contains* the "expected error" message. * * @param referenceFuncAsync a reference function implemented in pure * JavaScript/TypeScript which accepts N arguments and returns the "expected * result" or throws/rejects with the "expected error". * @param testFuncAsync a test function which, e.g., makes a call or sends a * transaction to a contract. It accepts the same N arguments returns the * "actual result" or throws/rejects with the "actual error". * @param values an array of N values, where each value corresponds in-order to * an argument to both the test function and the reference function. * @return A Promise that resolves if the test passes and rejects if the test * fails, according to the rules described above. */ export async function testWithReferenceFuncAsync( referenceFuncAsync: (...args: any[]) => Promise, testFuncAsync: (...args: any[]) => Promise, values: any[], ): Promise { let expectedResult: any; let expectedErr: string | undefined; try { expectedResult = await referenceFuncAsync(...values); } catch (e) { expectedErr = e.message; } let actualResult: any | undefined; try { actualResult = await testFuncAsync(...values); if (!_.isUndefined(expectedErr)) { throw new Error( `Expected error containing ${expectedErr} but got no error\n\tTest case: ${_getTestCaseString( referenceFuncAsync, values, )}`, ); } } catch (e) { if (_.isUndefined(expectedErr)) { throw new Error(`${e.message}\n\tTest case: ${_getTestCaseString(referenceFuncAsync, values)}`); } else { expect(e.message).to.contain( expectedErr, `${e.message}\n\tTest case: ${_getTestCaseString(referenceFuncAsync, values)}`, ); } } if (!_.isUndefined(actualResult) && !_.isUndefined(expectedResult)) { expect(actualResult).to.deep.equal( expectedResult, `Test case: ${_getTestCaseString(referenceFuncAsync, values)}`, ); } } function _getTestCaseString(referenceFuncAsync: (...args: any[]) => Promise, values: any[]): string { const paramNames = _getParameterNames(referenceFuncAsync); return JSON.stringify(_.zipObject(paramNames, values)); } // Source: https://stackoverflow.com/questions/1007981/how-to-get-function-parameter-names-values-dynamically function _getParameterNames(func: (...args: any[]) => any): string[] { return _.toString(func) .replace(/[/][/].*$/gm, '') // strip single-line comments .replace(/\s+/g, '') // strip white space .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments .split('){', 1)[0] .replace(/^[^(]*[(]/, '') // extract the parameters .replace(/=[^,]+/g, '') // strip any ES6 defaults .split(',') .filter(Boolean); // split & filter [""] }