assert: add direct promises support in rejects

This adds direct promise support to `assert.rejects` and
`assert.doesNotReject`. It will now accept both, functions and ES2015
promises as input.

Besides this the documentation was updated to reflect the latest
changes.

It also refactors the tests to a non blocking way to improve the
execution performance and improves the coverage.

PR-URL: https://github.com/nodejs/node/pull/19885
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
This commit is contained in:
Ruben Bridgewater 2018-04-08 23:28:30 +02:00
parent 11819c7773
commit 2c3146d06d
No known key found for this signature in database
GPG Key ID: F07496B3EB3C1762
3 changed files with 161 additions and 102 deletions

View File

@ -378,22 +378,23 @@ parameter is an instance of an [`Error`][] then it will be thrown instead of the
<!-- YAML <!-- YAML
added: REPLACEME added: REPLACEME
--> -->
* `block` {Function} * `block` {Function|Promise}
* `error` {RegExp|Function} * `error` {RegExp|Function}
* `message` {any} * `message` {any}
Awaits for the promise returned by function `block` to complete and not be Awaits the `block` promise or, if `block` is a function, immediately calls the
rejected. function and awaits the returned promise to complete. It will then check that
the promise is not rejected.
If `block` is a function and it throws an error synchronously,
`assert.doesNotReject()` will return a rejected Promise with that error without
checking the error handler.
Please note: Using `assert.doesNotReject()` is actually not useful because there Please note: Using `assert.doesNotReject()` is actually not useful because there
is little benefit by catching a rejection and then rejecting it again. Instead, is little benefit by catching a rejection and then rejecting it again. Instead,
consider adding a comment next to the specific code path that should not reject consider adding a comment next to the specific code path that should not reject
and keep error messages as expressive as possible. and keep error messages as expressive as possible.
When `assert.doesNotReject()` is called, it will immediately call the `block`
function, and awaits for completion. See [`assert.rejects()`][] for more
details.
Besides the async nature to await the completion behaves identically to Besides the async nature to await the completion behaves identically to
[`assert.doesNotThrow()`][]. [`assert.doesNotThrow()`][].
@ -409,12 +410,10 @@ Besides the async nature to await the completion behaves identically to
``` ```
```js ```js
assert.doesNotReject( assert.doesNotReject(Promise.reject(new TypeError('Wrong value')))
() => Promise.reject(new TypeError('Wrong value')), .then(() => {
SyntaxError
).then(() => {
// ... // ...
}); });
``` ```
## assert.doesNotThrow(block[, error][, message]) ## assert.doesNotThrow(block[, error][, message])
@ -916,14 +915,17 @@ assert(0);
<!-- YAML <!-- YAML
added: REPLACEME added: REPLACEME
--> -->
* `block` {Function} * `block` {Function|Promise}
* `error` {RegExp|Function|Object} * `error` {RegExp|Function|Object}
* `message` {any} * `message` {any}
Awaits for promise returned by function `block` to be rejected. Awaits the `block` promise or, if `block` is a function, immediately calls the
function and awaits the returned promise to complete. It will then check that
the promise is rejected.
When `assert.rejects()` is called, it will immediately call the `block` If `block` is a function and it throws an error synchronously,
function, and awaits for completion. `assert.rejects()` will return a rejected Promise with that error without
checking the error handler.
Besides the async nature to await the completion behaves identically to Besides the async nature to await the completion behaves identically to
[`assert.throws()`][]. [`assert.throws()`][].
@ -938,22 +940,31 @@ the block fails to reject.
(async () => { (async () => {
await assert.rejects( await assert.rejects(
async () => { async () => {
throw new Error('Wrong value'); throw new TypeError('Wrong value');
}, },
Error {
name: 'TypeError',
message: 'Wrong value'
}
); );
})(); })();
``` ```
```js ```js
assert.rejects( assert.rejects(
() => Promise.reject(new Error('Wrong value')), Promise.reject(new Error('Wrong value')),
Error Error
).then(() => { ).then(() => {
// ... // ...
}); });
``` ```
Note that `error` cannot 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
example in [`assert.throws()`][] carefully if using a string as the second
argument gets considered.
## assert.strictEqual(actual, expected[, message]) ## assert.strictEqual(actual, expected[, message])
<!-- YAML <!-- YAML
added: v0.1.21 added: v0.1.21
@ -1069,7 +1080,7 @@ assert.throws(
); );
``` ```
Note that `error` can not be a string. If a string is provided as the second Note that `error` cannot 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 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 `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 example below carefully if using a string as the second argument gets
@ -1123,7 +1134,6 @@ second argument. This might lead to difficult-to-spot errors.
[`assert.notDeepStrictEqual()`]: #assert_assert_notdeepstrictequal_actual_expected_message [`assert.notDeepStrictEqual()`]: #assert_assert_notdeepstrictequal_actual_expected_message
[`assert.notStrictEqual()`]: #assert_assert_notstrictequal_actual_expected_message [`assert.notStrictEqual()`]: #assert_assert_notstrictequal_actual_expected_message
[`assert.ok()`]: #assert_assert_ok_value_message [`assert.ok()`]: #assert_assert_ok_value_message
[`assert.rejects()`]: #assert_assert_rejects_block_error_message
[`assert.strictEqual()`]: #assert_assert_strictequal_actual_expected_message [`assert.strictEqual()`]: #assert_assert_strictequal_actual_expected_message
[`assert.throws()`]: #assert_assert_throws_block_error_message [`assert.throws()`]: #assert_assert_throws_block_error_message
[`strict mode`]: #assert_strict_mode [`strict mode`]: #assert_strict_mode

View File

@ -34,7 +34,7 @@ const {
} }
} = require('internal/errors'); } = require('internal/errors');
const { openSync, closeSync, readSync } = require('fs'); const { openSync, closeSync, readSync } = require('fs');
const { inspect } = require('util'); const { inspect, types: { isPromise } } = require('util');
const { EOL } = require('os'); const { EOL } = require('os');
const { NativeModule } = require('internal/bootstrap/loaders'); const { NativeModule } = require('internal/bootstrap/loaders');
@ -440,13 +440,27 @@ function getActual(block) {
return NO_EXCEPTION_SENTINEL; return NO_EXCEPTION_SENTINEL;
} }
function checkIsPromise(obj) {
// Accept native ES6 promises and promises that are implemented in a similar
// way. Do not accept thenables that use a function as `obj` and that have no
// `catch` handler.
return isPromise(obj) ||
obj !== null && typeof obj === 'object' &&
typeof obj.then === 'function' &&
typeof obj.catch === 'function';
}
async function waitForActual(block) { async function waitForActual(block) {
if (typeof block !== 'function') { let resultPromise;
throw new ERR_INVALID_ARG_TYPE('block', 'Function', block); if (typeof block === 'function') {
// Return a rejected promise if `block` throws synchronously.
resultPromise = block();
} else if (checkIsPromise(block)) {
resultPromise = block;
} else {
throw new ERR_INVALID_ARG_TYPE('block', ['Function', 'Promise'], block);
} }
// Return a rejected promise if `block` throws synchronously.
const resultPromise = block();
try { try {
await resultPromise; await resultPromise;
} catch (e) { } catch (e) {
@ -485,7 +499,7 @@ function expectsError(stackStartFn, actual, error, message) {
details += ` (${error.name})`; details += ` (${error.name})`;
} }
details += message ? `: ${message}` : '.'; details += message ? `: ${message}` : '.';
const fnType = stackStartFn === rejects ? 'rejection' : 'exception'; const fnType = stackStartFn.name === 'rejects' ? 'rejection' : 'exception';
innerFail({ innerFail({
actual: undefined, actual: undefined,
expected: error, expected: error,
@ -510,7 +524,8 @@ function expectsNoError(stackStartFn, actual, error, message) {
if (!error || expectedException(actual, error)) { if (!error || expectedException(actual, error)) {
const details = message ? `: ${message}` : '.'; const details = message ? `: ${message}` : '.';
const fnType = stackStartFn === doesNotReject ? 'rejection' : 'exception'; const fnType = stackStartFn.name === 'doesNotReject' ?
'rejection' : 'exception';
innerFail({ innerFail({
actual, actual,
expected: error, expected: error,
@ -523,29 +538,21 @@ function expectsNoError(stackStartFn, actual, error, message) {
throw actual; throw actual;
} }
function throws(block, ...args) { assert.throws = function throws(block, ...args) {
expectsError(throws, getActual(block), ...args); expectsError(throws, getActual(block), ...args);
} };
assert.throws = throws; assert.rejects = async function rejects(block, ...args) {
async function rejects(block, ...args) {
expectsError(rejects, await waitForActual(block), ...args); expectsError(rejects, await waitForActual(block), ...args);
} };
assert.rejects = rejects; assert.doesNotThrow = function doesNotThrow(block, ...args) {
function doesNotThrow(block, ...args) {
expectsNoError(doesNotThrow, getActual(block), ...args); expectsNoError(doesNotThrow, getActual(block), ...args);
} };
assert.doesNotThrow = doesNotThrow; assert.doesNotReject = async function doesNotReject(block, ...args) {
async function doesNotReject(block, ...args) {
expectsNoError(doesNotReject, await waitForActual(block), ...args); expectsNoError(doesNotReject, await waitForActual(block), ...args);
} };
assert.doesNotReject = doesNotReject;
assert.ifError = function ifError(err) { assert.ifError = function ifError(err) {
if (err !== null && err !== undefined) { if (err !== null && err !== undefined) {

View File

@ -1,74 +1,116 @@
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
const assert = require('assert'); const assert = require('assert');
const { promisify } = require('util');
const wait = promisify(setTimeout);
/* eslint-disable prefer-common-expectserror, no-restricted-properties */
// Test assert.rejects() and assert.doesNotReject() by checking their // Test assert.rejects() and assert.doesNotReject() by checking their
// expected output and by verifying that they do not work sync // expected output and by verifying that they do not work sync
common.crashOnUnhandledRejection(); common.crashOnUnhandledRejection();
(async () => { // Run all tests in parallel and check their outcome at the end.
await assert.rejects( const promises = [];
async () => assert.fail(),
common.expectsError({ // Check `assert.rejects`.
{
const rejectingFn = async () => assert.fail();
const errObj = {
code: 'ERR_ASSERTION', code: 'ERR_ASSERTION',
type: assert.AssertionError, name: 'AssertionError [ERR_ASSERTION]',
message: 'Failed' message: 'Failed'
}) };
); // `assert.rejects` accepts a function or a promise as first argument.
promises.push(assert.rejects(rejectingFn, errObj));
promises.push(assert.rejects(rejectingFn(), errObj));
}
await assert.doesNotReject(() => {}); {
const handler = (err) => {
{
const promise = assert.doesNotReject(async () => {
await wait(1);
throw new Error();
});
await assert.rejects(
() => promise,
(err) => {
assert(err instanceof assert.AssertionError, assert(err instanceof assert.AssertionError,
`${err.name} is not instance of AssertionError`); `${err.name} is not instance of AssertionError`);
assert.strictEqual(err.code, 'ERR_ASSERTION'); assert.strictEqual(err.code, 'ERR_ASSERTION');
assert.strictEqual(err.message, assert.strictEqual(err.message,
'Got unwanted rejection.\nActual message: ""'); 'Missing expected rejection (handler).');
assert.strictEqual(err.operator, 'doesNotReject');
assert.ok(!err.stack.includes('at Function.doesNotReject'));
return true;
}
);
}
{
const promise = assert.rejects(() => {});
await assert.rejects(
() => promise,
(err) => {
assert(err instanceof assert.AssertionError,
`${err.name} is not instance of AssertionError`);
assert.strictEqual(err.code, 'ERR_ASSERTION');
assert(/^Missing expected rejection\.$/.test(err.message));
assert.strictEqual(err.operator, 'rejects'); assert.strictEqual(err.operator, 'rejects');
assert.ok(!err.stack.includes('at Function.rejects')); assert.ok(!err.stack.includes('at Function.rejects'));
return true; return true;
} };
);
}
{ let promise = assert.rejects(async () => {}, handler);
promises.push(assert.rejects(promise, handler));
promise = assert.rejects(() => {}, handler);
promises.push(assert.rejects(promise, handler));
promise = assert.rejects(Promise.resolve(), handler);
promises.push(assert.rejects(promise, handler));
}
{
const THROWN_ERROR = new Error(); const THROWN_ERROR = new Error();
await assert.rejects(() => { promises.push(assert.rejects(() => {
throw THROWN_ERROR; throw THROWN_ERROR;
}).then(common.mustNotCall()) }).catch(common.mustCall((err) => {
.catch(
common.mustCall((err) => {
assert.strictEqual(err, THROWN_ERROR); assert.strictEqual(err, THROWN_ERROR);
}) })));
); }
promises.push(assert.rejects(
assert.rejects('fail', {}),
{
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "block" argument must be one of type ' +
'Function or Promise. Received type string'
} }
})().then(common.mustCall()); ));
// Check `assert.doesNotReject`.
{
// `assert.doesNotReject` accepts a function or a promise as first argument.
promises.push(assert.doesNotReject(() => {}));
promises.push(assert.doesNotReject(async () => {}));
promises.push(assert.doesNotReject(Promise.resolve()));
}
{
const handler1 = (err) => {
assert(err instanceof assert.AssertionError,
`${err.name} is not instance of AssertionError`);
assert.strictEqual(err.code, 'ERR_ASSERTION');
assert.strictEqual(err.message, 'Failed');
return true;
};
const handler2 = (err) => {
assert(err instanceof assert.AssertionError,
`${err.name} is not instance of AssertionError`);
assert.strictEqual(err.code, 'ERR_ASSERTION');
assert.strictEqual(err.message,
'Got unwanted rejection.\nActual message: "Failed"');
assert.strictEqual(err.operator, 'doesNotReject');
assert.ok(!err.stack.includes('at Function.doesNotReject'));
return true;
};
const rejectingFn = async () => assert.fail();
let promise = assert.doesNotReject(rejectingFn, handler1);
promises.push(assert.rejects(promise, handler2));
promise = assert.doesNotReject(rejectingFn(), handler1);
promises.push(assert.rejects(promise, handler2));
promise = assert.doesNotReject(() => assert.fail(), common.mustNotCall());
promises.push(assert.rejects(promise, handler1));
}
promises.push(assert.rejects(
assert.doesNotReject(123),
{
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "block" argument must be one of type ' +
'Function or Promise. Received type number'
}
));
// Make sure all async code gets properly executed.
Promise.all(promises).then(common.mustCall());