@0x/contracts-test-utils: Update testWithReferenceFunctionAsync() to
support `RevertError`s.
This commit is contained in:
@@ -1,35 +1,8 @@
|
||||
import { BigNumber, RevertError } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { expect } from './chai_setup';
|
||||
|
||||
class Value<T> {
|
||||
public value: T;
|
||||
constructor(value: T) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: max-classes-per-file
|
||||
class ErrorMessage {
|
||||
public error: string;
|
||||
constructor(message: string) {
|
||||
this.error = message;
|
||||
}
|
||||
}
|
||||
|
||||
type PromiseResult<T> = Value<T> | ErrorMessage;
|
||||
|
||||
// TODO(albrow): This seems like a generic utility function that could exist in
|
||||
// lodash. We should replace it by a library implementation, or move it to our
|
||||
// own.
|
||||
async function evaluatePromiseAsync<T>(promise: Promise<T>): Promise<PromiseResult<T>> {
|
||||
try {
|
||||
return new Value<T>(await promise);
|
||||
} catch (e) {
|
||||
return new ErrorMessage(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function testWithReferenceFuncAsync<P0, R>(
|
||||
referenceFunc: (p0: P0) => Promise<R>,
|
||||
testFunc: (p0: P0) => Promise<R>,
|
||||
@@ -88,36 +61,86 @@ export async function testWithReferenceFuncAsync(
|
||||
testFuncAsync: (...args: any[]) => Promise<any>,
|
||||
values: any[],
|
||||
): Promise<void> {
|
||||
// Measure correct behaviour
|
||||
const expected = await evaluatePromiseAsync(referenceFuncAsync(...values));
|
||||
// Measure correct behavior
|
||||
let expected: any;
|
||||
let expectedError: Error | undefined;
|
||||
try {
|
||||
expected = await referenceFuncAsync(...values);
|
||||
} catch (err) {
|
||||
expectedError = err;
|
||||
}
|
||||
// Measure actual behavior
|
||||
let actual: any;
|
||||
let actualError: Error | undefined;
|
||||
try {
|
||||
actual = await testFuncAsync(...values);
|
||||
} catch (err) {
|
||||
actualError = err;
|
||||
}
|
||||
|
||||
// Measure actual behaviour
|
||||
const actual = await evaluatePromiseAsync(testFuncAsync(...values));
|
||||
|
||||
// Compare behaviour
|
||||
if (expected instanceof ErrorMessage) {
|
||||
// If we expected an error, check if the actual error message contains the
|
||||
// expected error message.
|
||||
if (!(actual instanceof ErrorMessage)) {
|
||||
throw new Error(
|
||||
`Expected error containing ${expected.error} but got no error\n\tTest case: ${_getTestCaseString(
|
||||
referenceFuncAsync,
|
||||
values,
|
||||
)}`,
|
||||
const testCaseString = _getTestCaseString(referenceFuncAsync, values);
|
||||
// Compare behavior
|
||||
if (expectedError !== undefined) {
|
||||
// Expecting an error.
|
||||
if (actualError === undefined) {
|
||||
return expect.fail(
|
||||
actualError,
|
||||
expectedError,
|
||||
`${testCaseString}: expected failure but instead succeeded`,
|
||||
);
|
||||
} else {
|
||||
if (expectedError instanceof RevertError) {
|
||||
// Expecting a RevertError.
|
||||
if (actualError instanceof RevertError) {
|
||||
if (!actualError.equals(expectedError)) {
|
||||
return expect.fail(
|
||||
actualError,
|
||||
expectedError,
|
||||
`${testCaseString}: expected error ${actualError.toString()} to equal ${expectedError.toString()}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return expect.fail(
|
||||
actualError,
|
||||
expectedError,
|
||||
`${testCaseString}: expected a RevertError but received an Error`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Expecing any Error type.
|
||||
if (actualError.message !== expectedError.message) {
|
||||
return expect.fail(
|
||||
actualError,
|
||||
expectedError,
|
||||
`${testCaseString}: expected error message '${actualError.message}' to equal '${expectedError.message}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Not expecting an error.
|
||||
if (actualError !== undefined) {
|
||||
return expect.fail(
|
||||
actualError,
|
||||
expectedError,
|
||||
`${testCaseString}: expected success but instead failed`,
|
||||
);
|
||||
}
|
||||
expect(actual.error).to.contain(
|
||||
expected.error,
|
||||
`${actual.error}\n\tTest case: ${_getTestCaseString(referenceFuncAsync, values)}`,
|
||||
);
|
||||
} else {
|
||||
// If we do not expect an error, compare actual and expected directly.
|
||||
expect(actual).to.deep.equal(expected, `Test case ${_getTestCaseString(referenceFuncAsync, values)}`);
|
||||
if (expected instanceof BigNumber) {
|
||||
// Technically we can do this with `deep.eq`, but this prints prettier
|
||||
// error messages for BigNumbers.
|
||||
expect(actual).to.bignumber.eq(expected, testCaseString);
|
||||
} else {
|
||||
expect(actual).to.deep.eq(expected, testCaseString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _getTestCaseString(referenceFuncAsync: (...args: any[]) => Promise<any>, values: any[]): string {
|
||||
const paramNames = _getParameterNames(referenceFuncAsync);
|
||||
while (paramNames.length < values.length) {
|
||||
paramNames.push(`${paramNames.length}`);
|
||||
}
|
||||
return JSON.stringify(_.zipObject(paramNames, values));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import * as chai from 'chai';
|
||||
import { AnyRevertError, StringRevertError } from '@0x/utils';
|
||||
|
||||
import { chaiSetup } from '../src/chai_setup';
|
||||
import { expect } from '../src';
|
||||
import { testWithReferenceFuncAsync } from '../src/test_with_reference';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
async function divAsync(x: number, y: number): Promise<number> {
|
||||
if (y === 0) {
|
||||
throw new Error('MathError: divide by zero');
|
||||
@@ -18,46 +15,77 @@ function alwaysValueFunc(value: number): (x: number, y: number) => Promise<numbe
|
||||
return async (x: number, y: number) => value;
|
||||
}
|
||||
|
||||
// returns an async function which always throws/rejects with the given error
|
||||
// message.
|
||||
function alwaysFailFunc(errMessage: string): (x: number, y: number) => Promise<number> {
|
||||
// returns an async function which always throws/rejects with the given error.
|
||||
function alwaysFailFunc(error: Error): (x: number, y: number) => Promise<number> {
|
||||
return async (x: number, y: number) => {
|
||||
throw new Error(errMessage);
|
||||
throw error;
|
||||
};
|
||||
}
|
||||
|
||||
describe('testWithReferenceFuncAsync', () => {
|
||||
it('passes when both succeed and actual === expected', async () => {
|
||||
await testWithReferenceFuncAsync(alwaysValueFunc(0.5), divAsync, [1, 2]);
|
||||
it('passes when both succeed and actual == expected', async () => {
|
||||
return testWithReferenceFuncAsync(alwaysValueFunc(0.5), divAsync, [1, 2]);
|
||||
});
|
||||
|
||||
it('passes when both fail and actual error contains expected error', async () => {
|
||||
await testWithReferenceFuncAsync(alwaysFailFunc('divide by zero'), divAsync, [1, 0]);
|
||||
it('fails when both succeed and actual != expected', async () => {
|
||||
return expect(testWithReferenceFuncAsync(alwaysValueFunc(3), divAsync, [1, 2]))
|
||||
.to.be.rejectedWith('{"x":1,"y":2}: expected 0.5 to deeply equal 3');
|
||||
});
|
||||
|
||||
it('fails when both succeed and actual !== expected', async () => {
|
||||
expect(testWithReferenceFuncAsync(alwaysValueFunc(3), divAsync, [1, 2])).to.be.rejectedWith(
|
||||
'Test case {"x":1,"y":2}: expected { value: 0.5 } to deeply equal { value: 3 }',
|
||||
);
|
||||
it('passes when both fail and error messages are the same', async () => {
|
||||
const err = new Error('whoopsie');
|
||||
return testWithReferenceFuncAsync(alwaysFailFunc(err), alwaysFailFunc(err), [1, 1]);
|
||||
});
|
||||
|
||||
it('fails when both fail and actual error does not contain expected error', async () => {
|
||||
expect(
|
||||
testWithReferenceFuncAsync(alwaysFailFunc('Unexpected math error'), divAsync, [1, 0]),
|
||||
it('fails when both fail and error messages are not identical', async () => {
|
||||
const errorMessage = 'whoopsie';
|
||||
const notErrorMessage = 'not whoopsie';
|
||||
const error = new Error(errorMessage);
|
||||
const notError = new Error(notErrorMessage);
|
||||
return expect(
|
||||
testWithReferenceFuncAsync(alwaysFailFunc(notError), alwaysFailFunc(error), [1, 2]),
|
||||
).to.be.rejectedWith(
|
||||
'MathError: divide by zero\n\tTest case: {"x":1,"y":0}: expected \'MathError: divide by zero\' to include \'Unexpected math error\'',
|
||||
`{"x":1,"y":2}: expected error message '${errorMessage}' to equal '${notErrorMessage}'`,
|
||||
);
|
||||
});
|
||||
|
||||
it('passes when both fail with compatible RevertErrors', async () => {
|
||||
const error1 = new StringRevertError('whoopsie');
|
||||
const error2 = new AnyRevertError();
|
||||
return testWithReferenceFuncAsync(alwaysFailFunc(error1), alwaysFailFunc(error2), [1, 1]);
|
||||
});
|
||||
|
||||
it('fails when both fail with incompatible RevertErrors', async () => {
|
||||
const error1 = new StringRevertError('whoopsie');
|
||||
const error2 = new StringRevertError('not whoopsie');
|
||||
return expect(testWithReferenceFuncAsync(alwaysFailFunc(error1), alwaysFailFunc(error2), [1, 1]))
|
||||
.to.be.rejectedWith(
|
||||
`{"x":1,"y":1}: expected error StringRevertError({ message: 'not whoopsie' }) to equal StringRevertError({ message: 'whoopsie' })`,
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when reference function fails with a RevertError but test function fails with a regular Error', async () => {
|
||||
const error1 = new StringRevertError('whoopsie');
|
||||
const error2 = new Error('whoopsie');
|
||||
return expect(testWithReferenceFuncAsync(alwaysFailFunc(error1), alwaysFailFunc(error2), [1, 1]))
|
||||
.to.be.rejectedWith(
|
||||
`{"x":1,"y":1}: expected a RevertError but received an Error`,
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when referenceFunc succeeds and testFunc fails', async () => {
|
||||
expect(testWithReferenceFuncAsync(alwaysValueFunc(0), divAsync, [1, 0])).to.be.rejectedWith(
|
||||
'Test case {"x":1,"y":0}: expected { error: \'MathError: divide by zero\' } to deeply equal { value: 0 }',
|
||||
);
|
||||
const error = new Error('whoopsie');
|
||||
return expect(testWithReferenceFuncAsync(alwaysValueFunc(0), alwaysFailFunc(error), [1, 2]))
|
||||
.to.be.rejectedWith(
|
||||
`{"x":1,"y":2}: expected success but instead failed`,
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when referenceFunc fails and testFunc succeeds', async () => {
|
||||
expect(testWithReferenceFuncAsync(alwaysFailFunc('divide by zero'), divAsync, [1, 2])).to.be.rejectedWith(
|
||||
'Expected error containing divide by zero but got no error\n\tTest case: {"x":1,"y":2}',
|
||||
);
|
||||
const error = new Error('whoopsie');
|
||||
return expect(testWithReferenceFuncAsync(alwaysFailFunc(error), divAsync, [1, 2]))
|
||||
.to.be.rejectedWith(
|
||||
'{"x":1,"y":2}: expected failure but instead succeeded',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user