Skip to content

Commit f2af930

Browse files
BridgeARMylesBorins
authored andcommitted
assert: .throws accept objects
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 <[email protected]> Reviewed-By: Luigi Pinca <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Ron Korving <[email protected]> Reviewed-By: Yuta Hiroto <[email protected]>
1 parent 147aeed commit f2af930

File tree

3 files changed

+138
-17
lines changed

3 files changed

+138
-17
lines changed

doc/api/assert.md

+23-3
Original file line numberDiff line numberDiff line change
@@ -635,18 +635,21 @@ If the values are not strictly equal, an `AssertionError` is thrown with a
635635
<!-- YAML
636636
added: v0.1.21
637637
changes:
638+
- version: REPLACEME
639+
pr-url: https://github.com/nodejs/node/pull/REPLACEME
640+
description: The `error` parameter can now be an object as well.
638641
- version: v4.2.0
639642
pr-url: https://github.com/nodejs/node/pull/3276
640643
description: The `error` parameter can now be an arrow function.
641644
-->
642645
* `block` {Function}
643-
* `error` {RegExp|Function}
646+
* `error` {RegExp|Function|object}
644647
* `message` {any}
645648

646649
Expects the function `block` to throw an error.
647650

648-
If specified, `error` can be a constructor, [`RegExp`][], or validation
649-
function.
651+
If specified, `error` can be a constructor, [`RegExp`][], a validation
652+
function, or an object where each property will be tested for.
650653

651654
If specified, `message` will be the message provided by the `AssertionError` if
652655
the block fails to throw.
@@ -689,6 +692,23 @@ assert.throws(
689692
);
690693
```
691694

695+
Custom error object / error instance:
696+
697+
```js
698+
assert.throws(
699+
() => {
700+
const err = new TypeError('Wrong value');
701+
err.code = 404;
702+
throw err;
703+
},
704+
{
705+
name: 'TypeError',
706+
message: 'Wrong value'
707+
// Note that only properties on the error object will be tested!
708+
}
709+
);
710+
```
711+
692712
Note that `error` can not be a string. If a string is provided as the second
693713
argument, then `error` is assumed to be omitted and the string will be used for
694714
`message` instead. This can lead to easy-to-miss mistakes. Please read the

lib/assert.js

+39-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const { isSet, isMap, isDate, isRegExp } = process.binding('util');
2525
const { objectToString } = require('internal/util');
2626
const { isArrayBufferView } = require('internal/util/types');
2727
const errors = require('internal/errors');
28+
const { inspect } = require('util');
2829

2930
// The assert module provides functions that throw
3031
// AssertionError's when particular conditions are not met. The
@@ -660,10 +661,44 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
660661
}
661662
};
662663

663-
function expectedException(actual, expected) {
664+
function createMsg(msg, key, actual, expected) {
665+
if (msg)
666+
return msg;
667+
return `${key}: expected ${inspect(expected[key])}, ` +
668+
`not ${inspect(actual[key])}`;
669+
}
670+
671+
function expectedException(actual, expected, msg) {
664672
if (typeof expected !== 'function') {
665-
// Should be a RegExp, if not fail hard
666-
return expected.test(actual);
673+
if (expected instanceof RegExp)
674+
return expected.test(actual);
675+
// assert.doesNotThrow does not accept objects.
676+
if (arguments.length === 2) {
677+
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'expected',
678+
['Function', 'RegExp'], expected);
679+
}
680+
// The name and message could be non enumerable. Therefore test them
681+
// explicitly.
682+
if ('name' in expected) {
683+
assert.strictEqual(
684+
actual.name,
685+
expected.name,
686+
createMsg(msg, 'name', actual, expected));
687+
}
688+
if ('message' in expected) {
689+
assert.strictEqual(
690+
actual.message,
691+
expected.message,
692+
createMsg(msg, 'message', actual, expected));
693+
}
694+
const keys = Object.keys(expected);
695+
for (const key of keys) {
696+
assert.deepStrictEqual(
697+
actual[key],
698+
expected[key],
699+
createMsg(msg, key, actual, expected));
700+
}
701+
return true;
667702
}
668703
// Guard instanceof against arrow functions as they don't have a prototype.
669704
if (expected.prototype !== undefined && actual instanceof expected) {
@@ -716,7 +751,7 @@ assert.throws = function throws(block, error, message) {
716751
stackStartFn: throws
717752
});
718753
}
719-
if (error && expectedException(actual, error) === false) {
754+
if (error && expectedException(actual, error, message) === false) {
720755
throw actual;
721756
}
722757
};

test/parallel/test-assert.js

+76-10
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,6 @@ assert.ok(a.AssertionError.prototype instanceof Error,
3636

3737
assert.throws(makeBlock(a, false), a.AssertionError, 'ok(false)');
3838

39-
// Using a object as second arg results in a failure
40-
assert.throws(
41-
() => { assert.throws(() => { throw new Error(); }, { foo: 'bar' }); },
42-
common.expectsError({
43-
type: TypeError,
44-
message: 'expected.test is not a function'
45-
})
46-
);
47-
48-
4939
assert.doesNotThrow(makeBlock(a, true), a.AssertionError, 'ok(true)');
5040

5141
assert.doesNotThrow(makeBlock(a, 'test', 'ok(\'test\')'));
@@ -742,3 +732,79 @@ common.expectsError(
742732
'Received type string'
743733
}
744734
);
735+
736+
{
737+
const errFn = () => {
738+
const err = new TypeError('Wrong value');
739+
err.code = 404;
740+
throw err;
741+
};
742+
const errObj = {
743+
name: 'TypeError',
744+
message: 'Wrong value'
745+
};
746+
assert.throws(errFn, errObj);
747+
748+
errObj.code = 404;
749+
assert.throws(errFn, errObj);
750+
751+
errObj.code = '404';
752+
common.expectsError(
753+
// eslint-disable-next-line no-restricted-syntax
754+
() => assert.throws(errFn, errObj),
755+
{
756+
code: 'ERR_ASSERTION',
757+
type: assert.AssertionError,
758+
message: 'code: expected \'404\', not 404'
759+
}
760+
);
761+
762+
errObj.code = 404;
763+
errObj.foo = 'bar';
764+
common.expectsError(
765+
// eslint-disable-next-line no-restricted-syntax
766+
() => assert.throws(errFn, errObj),
767+
{
768+
code: 'ERR_ASSERTION',
769+
type: assert.AssertionError,
770+
message: 'foo: expected \'bar\', not undefined'
771+
}
772+
);
773+
774+
common.expectsError(
775+
() => assert.throws(() => { throw new Error(); }, { foo: 'bar' }, 'foobar'),
776+
{
777+
type: assert.AssertionError,
778+
code: 'ERR_ASSERTION',
779+
message: 'foobar'
780+
}
781+
);
782+
783+
common.expectsError(
784+
() => assert.doesNotThrow(() => { throw new Error(); }, { foo: 'bar' }),
785+
{
786+
type: TypeError,
787+
code: 'ERR_INVALID_ARG_TYPE',
788+
message: 'The "expected" argument must be one of type Function or ' +
789+
'RegExp. Received type object'
790+
}
791+
);
792+
793+
assert.throws(() => { throw new Error('e'); }, new Error('e'));
794+
common.expectsError(
795+
() => assert.throws(() => { throw new TypeError('e'); }, new Error('e')),
796+
{
797+
type: assert.AssertionError,
798+
code: 'ERR_ASSERTION',
799+
message: "name: expected 'Error', not 'TypeError'"
800+
}
801+
);
802+
common.expectsError(
803+
() => assert.throws(() => { throw new Error('foo'); }, new Error('')),
804+
{
805+
type: assert.AssertionError,
806+
code: 'ERR_ASSERTION',
807+
message: "message: expected '', not 'foo'"
808+
}
809+
);
810+
}

0 commit comments

Comments
 (0)