Skip to content

Commit

Permalink
assert: .throws accept objects
Browse files Browse the repository at this point in the history
From now on it is possible to use a validation object in throws
instead of the other possibilites.

Backport-PR-URL: #23223
PR-URL: #17584
Refs: #17557
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Ron Korving <ron@ronkorving.nl>
Reviewed-By: Yuta Hiroto <hello@about-hiroppy.com>
  • Loading branch information
BridgeAR authored and MylesBorins committed Oct 31, 2018
1 parent 147aeed commit f2af930
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 17 deletions.
26 changes: 23 additions & 3 deletions doc/api/assert.md
Original file line number Diff line number Diff line change
Expand Up @@ -635,18 +635,21 @@ If the values are not strictly equal, an `AssertionError` is thrown with a
<!-- YAML
added: v0.1.21
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/REPLACEME
description: The `error` parameter can now be an object as well.
- version: v4.2.0
pr-url: https://github.com/nodejs/node/pull/3276
description: The `error` parameter can now be an arrow function.
-->
* `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.
Expand Down Expand Up @@ -689,6 +692,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
Expand Down
43 changes: 39 additions & 4 deletions lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -660,10 +661,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) {
Expand Down Expand Up @@ -716,7 +751,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;
}
};
Expand Down
86 changes: 76 additions & 10 deletions test/parallel/test-assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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\')'));
Expand Down Expand Up @@ -742,3 +732,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'"
}
);
}

0 comments on commit f2af930

Please sign in to comment.