Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

assert: improve assert.throws #17585

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions doc/api/assert.md
Original file line number Diff line number Diff line change
Expand Up @@ -740,12 +740,15 @@ assert.throws(

Validate error message using [`RegExp`][]:

Using a regular expression runs `.toString` on the error object, and will
therefore also include the error name.

```js
assert.throws(
() => {
throw new Error('Wrong value');
},
/value/
/^Error: Wrong value$/
);
```

Expand All @@ -767,17 +770,42 @@ assert.throws(

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:

<!-- eslint-disable no-restricted-syntax -->
```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.

[`Error.captureStackTrace`]: errors.html#errors_error_capturestacktrace_targetobject_constructoropt
[`Map`]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Map
[`Object.is()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
Expand Down
95 changes: 50 additions & 45 deletions lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,68 +208,73 @@ 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) {
return e;
}
}

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}` : '.';
innerFail({
actual,
expected,
operator: 'throws',
message: `Missing expected exception${details}`,
stackStartFn: assert.throws
});
}
if (expected && expectedException(actual, expected) === false) {
throw actual;
}
} else if (actual !== undefined) {
if (!expected || expectedException(actual, expected)) {
details = message ? `: ${message}` : '.';
innerFail({
actual,
expected,
operator: 'doesNotThrow',
message: `Got unwanted exception${details}\n${actual.message}`,
stackStartFn: assert.doesNotThrow
});
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) === 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; };
Expand Down
11 changes: 11 additions & 0 deletions test/parallel/test-assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -773,3 +773,14 @@ common.expectsError(
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'
}
);