diff --git a/doc/api/assert.md b/doc/api/assert.md index 43c7e8e2d429c8..75477516bfadfe 100644 --- a/doc/api/assert.md +++ b/doc/api/assert.md @@ -711,18 +711,21 @@ instead of the `AssertionError`. * `block` {Function} -* `error` {RegExp|Function} +* `error` {RegExp|Function|object} * `message` {any} Expects the function `block` to throw an error. -If specified, `error` can be a constructor, [`RegExp`][], or validation -function. +If specified, `error` can be a constructor, [`RegExp`][], a validation +function, or an object where each property will be tested for. If specified, `message` will be the message provided by the `AssertionError` if the block fails to throw. @@ -768,6 +771,23 @@ assert.throws( ); ``` +Custom error object / error instance: + +```js +assert.throws( + () => { + const err = new TypeError('Wrong value'); + err.code = 404; + throw err; + }, + { + name: 'TypeError', + message: 'Wrong value' + // Note that only properties on the error object will be tested! + } +); +``` + Note that `error` can not be a string. If a string is provided as the second argument, then `error` is assumed to be omitted and the string will be used for `message` instead. This can lead to easy-to-miss mistakes. Please read the diff --git a/lib/assert.js b/lib/assert.js index 916efcd1ef5abe..c4f73f40071d9b 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -23,6 +23,7 @@ const { isDeepEqual, isDeepStrictEqual } = require('internal/util/comparisons'); const errors = require('internal/errors'); +const { inspect } = require('util'); // The assert module provides functions that throw // AssertionError's when particular conditions are not met. The @@ -193,10 +194,44 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) { } }; -function expectedException(actual, expected) { +function createMsg(msg, key, actual, expected) { + if (msg) + return msg; + return `${key}: expected ${inspect(expected[key])}, ` + + `not ${inspect(actual[key])}`; +} + +function expectedException(actual, expected, msg) { if (typeof expected !== 'function') { - // Should be a RegExp, if not fail hard - return expected.test(actual); + if (expected instanceof RegExp) + return expected.test(actual); + // assert.doesNotThrow does not accept objects. + if (arguments.length === 2) { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'expected', + ['Function', 'RegExp'], expected); + } + // The name and message could be non enumerable. Therefore test them + // explicitly. + if ('name' in expected) { + assert.strictEqual( + actual.name, + expected.name, + createMsg(msg, 'name', actual, expected)); + } + if ('message' in expected) { + assert.strictEqual( + actual.message, + expected.message, + createMsg(msg, 'message', actual, expected)); + } + const keys = Object.keys(expected); + for (const key of keys) { + assert.deepStrictEqual( + actual[key], + expected[key], + createMsg(msg, key, actual, expected)); + } + return true; } // Guard instanceof against arrow functions as they don't have a prototype. if (expected.prototype !== undefined && actual instanceof expected) { @@ -249,7 +284,7 @@ assert.throws = function throws(block, error, message) { stackStartFn: throws }); } - if (error && expectedException(actual, error) === false) { + if (error && expectedException(actual, error, message) === false) { throw actual; } }; diff --git a/test/parallel/test-assert.js b/test/parallel/test-assert.js index 27714e22fc4c53..c7bbd9249da387 100644 --- a/test/parallel/test-assert.js +++ b/test/parallel/test-assert.js @@ -39,16 +39,6 @@ assert.ok(a.AssertionError.prototype instanceof Error, assert.throws(makeBlock(a, false), a.AssertionError, 'ok(false)'); -// Using a object as second arg results in a failure -assert.throws( - () => { assert.throws(() => { throw new Error(); }, { foo: 'bar' }); }, - common.expectsError({ - type: TypeError, - message: 'expected.test is not a function' - }) -); - - assert.doesNotThrow(makeBlock(a, true), a.AssertionError, 'ok(true)'); assert.doesNotThrow(makeBlock(a, 'test', 'ok(\'test\')')); @@ -784,3 +774,79 @@ common.expectsError( 'Received type string' } ); + +{ + const errFn = () => { + const err = new TypeError('Wrong value'); + err.code = 404; + throw err; + }; + const errObj = { + name: 'TypeError', + message: 'Wrong value' + }; + assert.throws(errFn, errObj); + + errObj.code = 404; + assert.throws(errFn, errObj); + + errObj.code = '404'; + common.expectsError( + // eslint-disable-next-line no-restricted-syntax + () => assert.throws(errFn, errObj), + { + code: 'ERR_ASSERTION', + type: assert.AssertionError, + message: 'code: expected \'404\', not 404' + } + ); + + errObj.code = 404; + errObj.foo = 'bar'; + common.expectsError( + // eslint-disable-next-line no-restricted-syntax + () => assert.throws(errFn, errObj), + { + code: 'ERR_ASSERTION', + type: assert.AssertionError, + message: 'foo: expected \'bar\', not undefined' + } + ); + + common.expectsError( + () => assert.throws(() => { throw new Error(); }, { foo: 'bar' }, 'foobar'), + { + type: assert.AssertionError, + code: 'ERR_ASSERTION', + message: 'foobar' + } + ); + + common.expectsError( + () => assert.doesNotThrow(() => { throw new Error(); }, { foo: 'bar' }), + { + type: TypeError, + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "expected" argument must be one of type Function or ' + + 'RegExp. Received type object' + } + ); + + assert.throws(() => { throw new Error('e'); }, new Error('e')); + common.expectsError( + () => assert.throws(() => { throw new TypeError('e'); }, new Error('e')), + { + type: assert.AssertionError, + code: 'ERR_ASSERTION', + message: "name: expected 'Error', not 'TypeError'" + } + ); + common.expectsError( + () => assert.throws(() => { throw new Error('foo'); }, new Error('')), + { + type: assert.AssertionError, + code: 'ERR_ASSERTION', + message: "message: expected '', not 'foo'" + } + ); +}