diff --git a/doc/api/assert.md b/doc/api/assert.md index d2b541111586be..dbd32096b5c096 100644 --- a/doc/api/assert.md +++ b/doc/api/assert.md @@ -7,6 +7,57 @@ The `assert` module provides a simple set of assertion tests that can be used to test invariants. +A `strict` and a `legacy` mode exist, while it is recommended to only use +[`strict mode`][]. + +For more information about the used equality comparisons see +[MDN's guide on equality comparisons and sameness][mdn-equality-guide]. + +## Strict mode + + +When using the `strict mode`, any `assert` function will use the equality used in +the strict function mode. So [`assert.deepEqual()`][] will, for example, work the +same as [`assert.deepStrictEqual()`][]. + +It can be accessed using: + +```js +const assert = require('assert').strict; +``` + +## Legacy mode + +> Stability: 0 - Deprecated: Use strict mode instead. + +When accessing `assert` directly instead of using the `strict` property, the +[Abstract Equality Comparison][] will be used for any function without a +"strict" in its name (e.g. [`assert.deepEqual()`][]). + +It can be accessed using: + +```js +const assert = require('assert'); +``` + +It is recommended to use the [`strict mode`][] instead as the +[Abstract Equality Comparison][] can often have surprising results. Especially +in case of [`assert.deepEqual()`][] as the used comparison rules there are very +lax. + +E.g. + +```js +// WARNING: This does not throw an AssertionError! +assert.deepEqual(/a/gi, new Date()); +``` + ## assert(value[, message]) * `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. @@ -596,19 +692,61 @@ 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: +`message` instead. This can lead to easy-to-miss mistakes. Please read the +example below carefully if using a string as the second argument gets +considered: ```js -// THIS IS A MISTAKE! DO NOT DO THIS! -assert.throws(myFunction, 'missing foo', 'did not throw with expected message'); - -// Do this instead. -assert.throws(myFunction, /missing foo/, 'did not throw with expected message'); +function throwingFirst() { + throw new Error('First'); +} +function throwingSecond() { + throw new Error('Second'); +} +function notThrowing() {} + +// The second argument is a string and the input function threw an Error. +// In that case both cases do not throw as neither is going to try to +// match for the error message thrown by the input function! +assert.throws(throwingFirst, 'Second'); +assert.throws(throwingSecond, 'Second'); + +// The string is only used (as message) in case the function does not throw: +assert.throws(notThrowing, 'Second'); +// AssertionError [ERR_ASSERTION]: Missing expected exception: Second + +// If it was intended to match for the error message do this instead: +assert.throws(throwingSecond, /Second$/); +// Does not throw because the error messages match. +assert.throws(throwingFirst, /Second$/); +// Throws a error: +// Error: First +// at throwingFirst (repl:2:9) ``` +Due to the confusing notation, it is recommended not to use a string as the +second argument. This might lead to difficult-to-spot errors. + ## Caveats For the following cases, consider using ES2015 [`Object.is()`][], @@ -643,8 +781,12 @@ For more information, see [`TypeError`]: errors.html#errors_class_typeerror [`assert.deepEqual()`]: #assert_assert_deepequal_actual_expected_message [`assert.deepStrictEqual()`]: #assert_assert_deepstrictequal_actual_expected_message +[`assert.notDeepStrictEqual()`]: #assert_assert_notdeepstrictequal_actual_expected_message +[`assert.notStrictEqual()`]: #assert_assert_notstrictequal_actual_expected_message [`assert.ok()`]: #assert_assert_ok_value_message +[`assert.strictEqual()`]: #assert_assert_strictequal_actual_expected_message [`assert.throws()`]: #assert_assert_throws_block_error_message +[`strict mode`]: #assert_strict_mode [Abstract Equality Comparison]: https://tc39.github.io/ecma262/#sec-abstract-equality-comparison [Object.prototype.toString()]: https://tc39.github.io/ecma262/#sec-object.prototype.tostring [SameValueZero]: https://tc39.github.io/ecma262/#sec-samevaluezero diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md index efa939c5779b55..12a73f8878c855 100644 --- a/doc/api/deprecations.md +++ b/doc/api/deprecations.md @@ -684,6 +684,15 @@ Type: Runtime cause a lot of issues. See https://github.com/nodejs/node/issues/14328 for more details. + +### DEP0089: require('assert') + +Type: Documentation-only + +Importing assert directly is not recommended as the exposed functions will use +loose equality checks. Use `require('assert').strict` instead. The API is the +same as the legacy assert but it will always use strict equality checks. + ### DEP0098: AsyncHooks Embedder AsyncResource.emit{Before,After} APIs diff --git a/lib/assert.js b/lib/assert.js index 3418cf3230f83f..ad568547b45c6a 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -25,6 +25,7 @@ const { isSet, isMap, isDate, isRegExp } = process.binding('util'); const { objectToString } = require('internal/util'); const { isArrayBufferView } = require('internal/util/types'); 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 @@ -38,22 +39,26 @@ const assert = module.exports = ok; // both the actual and expected values to the assertion error for // display purposes. -function innerFail(actual, expected, message, operator, stackStartFunction) { - throw new errors.AssertionError({ - message, - actual, - expected, - operator, - stackStartFunction - }); +function innerFail(obj) { + throw new errors.AssertionError(obj); } -function fail(actual, expected, message, operator, stackStartFunction) { - if (arguments.length === 1) +function fail(actual, expected, message, operator, stackStartFn) { + const argsLen = arguments.length; + + if (argsLen === 1) { message = actual; - if (arguments.length === 2) + } else if (argsLen === 2) { operator = '!='; - innerFail(actual, expected, message, operator, stackStartFunction || fail); + } + + innerFail({ + actual, + expected, + message, + operator, + stackStartFn: stackStartFn || fail + }); } assert.fail = fail; @@ -67,7 +72,15 @@ assert.AssertionError = errors.AssertionError; // Pure assertion tests whether a value is truthy, as determined // by !!value. function ok(value, message) { - if (!value) innerFail(value, true, message, '==', ok); + if (!value) { + innerFail({ + actual: value, + expected: true, + message, + operator: '==', + stackStartFn: ok + }); + } } assert.ok = ok; @@ -75,7 +88,15 @@ assert.ok = ok; /* eslint-disable no-restricted-properties */ assert.equal = function equal(actual, expected, message) { // eslint-disable-next-line eqeqeq - if (actual != expected) innerFail(actual, expected, message, '==', equal); + if (actual != expected) { + innerFail({ + actual, + expected, + message, + operator: '==', + stackStartFn: equal + }); + } }; // The non-equality assertion tests for whether two objects are not @@ -83,21 +104,39 @@ assert.equal = function equal(actual, expected, message) { assert.notEqual = function notEqual(actual, expected, message) { // eslint-disable-next-line eqeqeq if (actual == expected) { - innerFail(actual, expected, message, '!=', notEqual); + innerFail({ + actual, + expected, + message, + operator: '!=', + stackStartFn: notEqual + }); } }; // The equivalence assertion tests a deep equality relation. assert.deepEqual = function deepEqual(actual, expected, message) { if (!innerDeepEqual(actual, expected, false)) { - innerFail(actual, expected, message, 'deepEqual', deepEqual); + innerFail({ + actual, + expected, + message, + operator: 'deepEqual', + stackStartFn: deepEqual + }); } }; /* eslint-enable */ assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) { if (!innerDeepEqual(actual, expected, true)) { - innerFail(actual, expected, message, 'deepStrictEqual', deepStrictEqual); + innerFail({ + actual, + expected, + message, + operator: 'deepStrictEqual', + stackStartFn: deepStrictEqual + }); } }; @@ -572,22 +611,39 @@ function objEquiv(a, b, strict, keys, memos) { // The non-equivalence assertion tests for any deep inequality. assert.notDeepEqual = function notDeepEqual(actual, expected, message) { if (innerDeepEqual(actual, expected, false)) { - innerFail(actual, expected, message, 'notDeepEqual', notDeepEqual); + innerFail({ + actual, + expected, + message, + operator: 'notDeepEqual', + stackStartFn: notDeepEqual + }); } }; assert.notDeepStrictEqual = notDeepStrictEqual; function notDeepStrictEqual(actual, expected, message) { if (innerDeepEqual(actual, expected, true)) { - innerFail(actual, expected, message, 'notDeepStrictEqual', - notDeepStrictEqual); + innerFail({ + actual, + expected, + message, + operator: 'notDeepStrictEqual', + stackStartFn: notDeepStrictEqual + }); } } // The strict equality assertion tests strict equality, as determined by ===. assert.strictEqual = function strictEqual(actual, expected, message) { if (actual !== expected) { - innerFail(actual, expected, message, '===', strictEqual); + innerFail({ + actual, + expected, + message, + operator: '===', + stackStartFn: strictEqual + }); } }; @@ -595,14 +651,50 @@ assert.strictEqual = function strictEqual(actual, expected, message) { // determined by !==. assert.notStrictEqual = function notStrictEqual(actual, expected, message) { if (actual === expected) { - innerFail(actual, expected, message, '!==', notStrictEqual); + innerFail({ + actual, + expected, + message, + operator: '!==', + stackStartFn: notStrictEqual + }); } }; -function expectedException(actual, expected) { +function compareExceptionKey(actual, expected, key, msg) { + if (!innerDeepEqual(actual[key], expected[key], true)) { + innerFail({ + actual: actual[key], + expected: expected[key], + message: msg || `${key}: expected ${inspect(expected[key])}, ` + + `not ${inspect(actual[key])}`, + operator: 'throws', + stackStartFn: assert.throws + }); + } +} + +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) { + compareExceptionKey(actual, expected, 'name', msg); + } + if ('message' in expected) { + compareExceptionKey(actual, expected, 'message', msg); + } + for (const key of Object.keys(expected)) { + compareExceptionKey(actual, expected, key, msg); + } + return true; } // Guard instanceof against arrow functions as they don't have a prototype. if (expected.prototype !== undefined && actual instanceof expected) { @@ -614,7 +706,11 @@ function expectedException(actual, expected) { return expected.call({}, actual) === true; } -function tryBlock(block) { +function getActual(block) { + if (typeof block !== 'function') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'block', 'Function', + block); + } try { block(); } catch (e) { @@ -622,48 +718,81 @@ function tryBlock(block) { } } -function innerThrows(shouldThrow, block, expected, message) { - var details = ''; +// Expected to throw an error. +assert.throws = function throws(block, error, message) { + const actual = getActual(block); - if (typeof block !== 'function') { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'block', 'function', - block); - } + if (typeof error === 'string') { + if (arguments.length === 3) + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', + 'error', + ['Function', 'RegExp'], + error); - if (typeof expected === 'string') { - message = expected; - expected = null; + message = error; + error = null; } - const actual = tryBlock(block); - - if (shouldThrow === true) { - if (actual === undefined) { - if (expected && expected.name) { - details += ` (${expected.name})`; - } - details += message ? `: ${message}` : '.'; - fail(actual, expected, `Missing expected exception${details}`, fail); - } - if (expected && expectedException(actual, expected) === false) { - throw actual; - } - } else if (actual !== undefined) { - if (!expected || expectedException(actual, expected)) { - details = message ? `: ${message}` : '.'; - fail(actual, expected, `Got unwanted exception${details}`, fail); + if (actual === undefined) { + let details = ''; + if (error && error.name) { + details += ` (${error.name})`; } + details += message ? `: ${message}` : '.'; + innerFail({ + actual, + expected: error, + operator: 'throws', + message: `Missing expected exception${details}`, + stackStartFn: throws + }); + } + if (error && expectedException(actual, error, message) === false) { throw actual; } -} - -// Expected to throw an error. -assert.throws = function throws(block, error, message) { - innerThrows(true, block, error, message); }; assert.doesNotThrow = function doesNotThrow(block, error, message) { - innerThrows(false, block, error, message); + const actual = getActual(block); + if (actual === undefined) + return; + + if (typeof error === 'string') { + message = error; + error = null; + } + + if (!error || expectedException(actual, error)) { + const details = message ? `: ${message}` : '.'; + innerFail({ + actual, + expected: error, + operator: 'doesNotThrow', + message: `Got unwanted exception${details}\n${actual.message}`, + stackStartFn: doesNotThrow + }); + } + throw actual; }; assert.ifError = function ifError(err) { if (err) throw err; }; + +// Expose a strict only variant of assert +function strict(value, message) { + if (!value) { + innerFail({ + actual: value, + expected: true, + message, + operator: '==', + stackStartFn: strict + }); + } +} +assert.strict = Object.assign(strict, assert, { + equal: assert.strictEqual, + deepEqual: assert.deepStrictEqual, + notEqual: assert.notStrictEqual, + notDeepEqual: assert.notDeepStrictEqual +}); +assert.strict.strict = assert.strict; diff --git a/lib/internal/errors.js b/lib/internal/errors.js index e62ce3fb22759a..590ab57bdb4e5d 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -80,7 +80,7 @@ class AssertionError extends Error { if (typeof options !== 'object' || options === null) { throw new exports.TypeError('ERR_INVALID_ARG_TYPE', 'options', 'object'); } - var { actual, expected, message, operator, stackStartFunction } = options; + var { actual, expected, message, operator, stackStartFn } = options; if (message) { super(message); } else { @@ -99,7 +99,7 @@ class AssertionError extends Error { this.actual = actual; this.expected = expected; this.operator = operator; - Error.captureStackTrace(this, stackStartFunction); + Error.captureStackTrace(this, stackStartFn); } } diff --git a/test/message/assert_throws_stack.js b/test/message/assert_throws_stack.js new file mode 100644 index 00000000000000..36bc5734cae37f --- /dev/null +++ b/test/message/assert_throws_stack.js @@ -0,0 +1,6 @@ +'use strict'; + +require('../common'); +const assert = require('assert').strict; + +assert.throws(() => { throw new Error('foo'); }, { bar: true }); diff --git a/test/message/assert_throws_stack.out b/test/message/assert_throws_stack.out new file mode 100644 index 00000000000000..04e62b98139eed --- /dev/null +++ b/test/message/assert_throws_stack.out @@ -0,0 +1,14 @@ +assert.js:* + throw new errors.AssertionError(obj); + ^ + +AssertionError [ERR_ASSERTION]: bar: expected true, not undefined + at Object. (*assert_throws_stack.js:*:*) + at * + at * + at * + at * + at * + at * + at * + at * diff --git a/test/message/error_exit.out b/test/message/error_exit.out index d6fbded760106b..c2dfc6a9f910ca 100644 --- a/test/message/error_exit.out +++ b/test/message/error_exit.out @@ -1,6 +1,6 @@ Exiting with code=1 assert.js:* - throw new errors.AssertionError({ + throw new errors.AssertionError(obj); ^ AssertionError [ERR_ASSERTION]: 1 === 2 diff --git a/test/parallel/test-assert.js b/test/parallel/test-assert.js index c73b49deb328cb..08b81b7c37f7fa 100644 --- a/test/parallel/test-assert.js +++ b/test/parallel/test-assert.js @@ -36,16 +36,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\')')); @@ -463,10 +453,15 @@ assert.throws(() => { assert.ifError(new Error('test error')); }, assert.doesNotThrow(() => { assert.ifError(null); }); assert.doesNotThrow(() => { assert.ifError(); }); -assert.throws(() => { - assert.doesNotThrow(makeBlock(thrower, Error), 'user message'); -}, /Got unwanted exception: user message/, - 'a.doesNotThrow ignores user message'); +common.expectsError( + () => assert.doesNotThrow(makeBlock(thrower, Error), 'user message'), + { + type: a.AssertionError, + code: 'ERR_ASSERTION', + operator: 'doesNotThrow', + message: 'Got unwanted exception: user message\n[object Object]' + } +); // make sure that validating using constructor really works { @@ -525,7 +520,8 @@ a.throws(makeBlock(thrower, TypeError), (err) => { () => { a.throws((noop)); }, common.expectsError({ code: 'ERR_ASSERTION', - message: /^Missing expected exception\.$/ + message: /^Missing expected exception\.$/, + operator: 'throws' })); assert.throws( @@ -597,6 +593,7 @@ testAssertionMessage({ a: NaN, b: Infinity, c: -Infinity }, } catch (e) { threw = true; assert.strictEqual(e.message, 'Missing expected exception.'); + assert.ok(!e.stack.includes('throws'), e.stack); } assert.ok(threw); } @@ -613,8 +610,8 @@ try { assert.strictEqual(1, 2, 'oh no'); // eslint-disable-line no-restricted-syntax } catch (e) { assert.strictEqual(e.message.split('\n')[0], 'oh no'); - // Message should not be marked as generated. - assert.strictEqual(e.generatedMessage, false); + assert.strictEqual(e.generatedMessage, false, + 'Message incorrectly marked as generated'); } { @@ -633,7 +630,7 @@ try { common.expectsError({ code: 'ERR_INVALID_ARG_TYPE', type: TypeError, - message: 'The "block" argument must be of type function. Received ' + + message: 'The "block" argument must be of type Function. Received ' + `type ${typeName(block)}` })(e); } @@ -696,3 +693,126 @@ common.expectsError( message: /^'Error: foo' === 'Error: foobar'$/ } ); + +// Test strict assert +{ + const a = require('assert'); + const assert = require('assert').strict; + /* eslint-disable no-restricted-properties */ + assert.throws(() => assert.equal(1, true), assert.AssertionError); + assert.notEqual(0, false); + assert.throws(() => assert.deepEqual(1, true), assert.AssertionError); + assert.notDeepEqual(0, false); + assert.equal(assert.strict, assert.strict.strict); + assert.equal(assert.equal, assert.strictEqual); + assert.equal(assert.deepEqual, assert.deepStrictEqual); + assert.equal(assert.notEqual, assert.notStrictEqual); + assert.equal(assert.notDeepEqual, assert.notDeepStrictEqual); + assert.equal(Object.keys(assert).length, Object.keys(a).length); + /* eslint-enable no-restricted-properties */ + assert(7); + common.expectsError( + () => assert(), + { + code: 'ERR_ASSERTION', + type: assert.AssertionError, + message: 'undefined == true' + } + ); +} + +common.expectsError( + () => assert.ok(null), + { + code: 'ERR_ASSERTION', + type: assert.AssertionError, + message: 'null == true' + } +); + +common.expectsError( + // eslint-disable-next-line no-restricted-syntax + () => assert.throws(() => {}, 'Error message', 'message'), + { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError, + message: 'The "error" argument must be one of type Function or RegExp. ' + + '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'" + } + ); +}