diff --git a/contracts/test-utils/src/test_with_reference.ts b/contracts/test-utils/src/test_with_reference.ts index 7585f8bd4e..7d0dc23521 100644 --- a/contracts/test-utils/src/test_with_reference.ts +++ b/contracts/test-utils/src/test_with_reference.ts @@ -1,35 +1,8 @@ +import { BigNumber, RevertError } from '@0x/utils'; import * as _ from 'lodash'; import { expect } from './chai_setup'; -class Value { - 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 = Value | 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(promise: Promise): Promise> { - try { - return new Value(await promise); - } catch (e) { - return new ErrorMessage(e.message); - } -} - export async function testWithReferenceFuncAsync( referenceFunc: (p0: P0) => Promise, testFunc: (p0: P0) => Promise, @@ -88,36 +61,86 @@ export async function testWithReferenceFuncAsync( testFuncAsync: (...args: any[]) => Promise, values: any[], ): Promise { - // 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, values: any[]): string { const paramNames = _getParameterNames(referenceFuncAsync); + while (paramNames.length < values.length) { + paramNames.push(`${paramNames.length}`); + } return JSON.stringify(_.zipObject(paramNames, values)); } diff --git a/contracts/test-utils/test/test_with_reference.ts b/contracts/test-utils/test/test_with_reference.ts index 1c1211003e..b3fbef3599 100644 --- a/contracts/test-utils/test/test_with_reference.ts +++ b/contracts/test-utils/test/test_with_reference.ts @@ -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 { if (y === 0) { throw new Error('MathError: divide by zero'); @@ -18,46 +15,77 @@ function alwaysValueFunc(value: number): (x: number, y: number) => Promise value; } -// returns an async function which always throws/rejects with the given error -// message. -function alwaysFailFunc(errMessage: string): (x: number, y: number) => Promise { +// returns an async function which always throws/rejects with the given error. +function alwaysFailFunc(error: Error): (x: number, y: number) => Promise { 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', + ); }); });