Skip to content

Commit b3c1bd3

Browse files
BridgeARjasnell
authored andcommitted
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: #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>
1 parent e3579a0 commit b3c1bd3

File tree

3 files changed

+161
-102
lines changed

3 files changed

+161
-102
lines changed

doc/api/assert.md

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -378,22 +378,23 @@ parameter is an instance of an [`Error`][] then it will be thrown instead of the
378378
<!-- YAML
379379
added: REPLACEME
380380
-->
381-
* `block` {Function}
381+
* `block` {Function|Promise}
382382
* `error` {RegExp|Function}
383383
* `message` {any}
384384

385-
Awaits for the promise returned by function `block` to complete and not be
386-
rejected.
385+
Awaits the `block` promise or, if `block` is a function, immediately calls the
386+
function and awaits the returned promise to complete. It will then check that
387+
the promise is not rejected.
388+
389+
If `block` is a function and it throws an error synchronously,
390+
`assert.doesNotReject()` will return a rejected Promise with that error without
391+
checking the error handler.
387392

388393
Please note: Using `assert.doesNotReject()` is actually not useful because there
389394
is little benefit by catching a rejection and then rejecting it again. Instead,
390395
consider adding a comment next to the specific code path that should not reject
391396
and keep error messages as expressive as possible.
392397

393-
When `assert.doesNotReject()` is called, it will immediately call the `block`
394-
function, and awaits for completion. See [`assert.rejects()`][] for more
395-
details.
396-
397398
Besides the async nature to await the completion behaves identically to
398399
[`assert.doesNotThrow()`][].
399400

@@ -409,12 +410,10 @@ Besides the async nature to await the completion behaves identically to
409410
```
410411

411412
```js
412-
assert.doesNotReject(
413-
() => Promise.reject(new TypeError('Wrong value')),
414-
SyntaxError
415-
).then(() => {
416-
// ...
417-
});
413+
assert.doesNotReject(Promise.reject(new TypeError('Wrong value')))
414+
.then(() => {
415+
// ...
416+
});
418417
```
419418

420419
## assert.doesNotThrow(block[, error][, message])
@@ -916,14 +915,17 @@ assert(0);
916915
<!-- YAML
917916
added: REPLACEME
918917
-->
919-
* `block` {Function}
918+
* `block` {Function|Promise}
920919
* `error` {RegExp|Function|Object}
921920
* `message` {any}
922921

923-
Awaits for promise returned by function `block` to be rejected.
922+
Awaits the `block` promise or, if `block` is a function, immediately calls the
923+
function and awaits the returned promise to complete. It will then check that
924+
the promise is rejected.
924925

925-
When `assert.rejects()` is called, it will immediately call the `block`
926-
function, and awaits for completion.
926+
If `block` is a function and it throws an error synchronously,
927+
`assert.rejects()` will return a rejected Promise with that error without
928+
checking the error handler.
927929

928930
Besides the async nature to await the completion behaves identically to
929931
[`assert.throws()`][].
@@ -938,22 +940,31 @@ the block fails to reject.
938940
(async () => {
939941
await assert.rejects(
940942
async () => {
941-
throw new Error('Wrong value');
943+
throw new TypeError('Wrong value');
942944
},
943-
Error
945+
{
946+
name: 'TypeError',
947+
message: 'Wrong value'
948+
}
944949
);
945950
})();
946951
```
947952

948953
```js
949954
assert.rejects(
950-
() => Promise.reject(new Error('Wrong value')),
955+
Promise.reject(new Error('Wrong value')),
951956
Error
952957
).then(() => {
953958
// ...
954959
});
955960
```
956961

962+
Note that `error` cannot be a string. If a string is provided as the second
963+
argument, then `error` is assumed to be omitted and the string will be used for
964+
`message` instead. This can lead to easy-to-miss mistakes. Please read the
965+
example in [`assert.throws()`][] carefully if using a string as the second
966+
argument gets considered.
967+
957968
## assert.strictEqual(actual, expected[, message])
958969
<!-- YAML
959970
added: v0.1.21
@@ -1069,7 +1080,7 @@ assert.throws(
10691080
);
10701081
```
10711082

1072-
Note that `error` can not be a string. If a string is provided as the second
1083+
Note that `error` cannot be a string. If a string is provided as the second
10731084
argument, then `error` is assumed to be omitted and the string will be used for
10741085
`message` instead. This can lead to easy-to-miss mistakes. Please read the
10751086
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.
11231134
[`assert.notDeepStrictEqual()`]: #assert_assert_notdeepstrictequal_actual_expected_message
11241135
[`assert.notStrictEqual()`]: #assert_assert_notstrictequal_actual_expected_message
11251136
[`assert.ok()`]: #assert_assert_ok_value_message
1126-
[`assert.rejects()`]: #assert_assert_rejects_block_error_message
11271137
[`assert.strictEqual()`]: #assert_assert_strictequal_actual_expected_message
11281138
[`assert.throws()`]: #assert_assert_throws_block_error_message
11291139
[`strict mode`]: #assert_strict_mode

lib/assert.js

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const {
3434
}
3535
} = require('internal/errors');
3636
const { openSync, closeSync, readSync } = require('fs');
37-
const { inspect } = require('util');
37+
const { inspect, types: { isPromise } } = require('util');
3838
const { EOL } = require('os');
3939
const { NativeModule } = require('internal/bootstrap/loaders');
4040

@@ -440,13 +440,27 @@ function getActual(block) {
440440
return NO_EXCEPTION_SENTINEL;
441441
}
442442

443+
function checkIsPromise(obj) {
444+
// Accept native ES6 promises and promises that are implemented in a similar
445+
// way. Do not accept thenables that use a function as `obj` and that have no
446+
// `catch` handler.
447+
return isPromise(obj) ||
448+
obj !== null && typeof obj === 'object' &&
449+
typeof obj.then === 'function' &&
450+
typeof obj.catch === 'function';
451+
}
452+
443453
async function waitForActual(block) {
444-
if (typeof block !== 'function') {
445-
throw new ERR_INVALID_ARG_TYPE('block', 'Function', block);
454+
let resultPromise;
455+
if (typeof block === 'function') {
456+
// Return a rejected promise if `block` throws synchronously.
457+
resultPromise = block();
458+
} else if (checkIsPromise(block)) {
459+
resultPromise = block;
460+
} else {
461+
throw new ERR_INVALID_ARG_TYPE('block', ['Function', 'Promise'], block);
446462
}
447463

448-
// Return a rejected promise if `block` throws synchronously.
449-
const resultPromise = block();
450464
try {
451465
await resultPromise;
452466
} catch (e) {
@@ -485,7 +499,7 @@ function expectsError(stackStartFn, actual, error, message) {
485499
details += ` (${error.name})`;
486500
}
487501
details += message ? `: ${message}` : '.';
488-
const fnType = stackStartFn === rejects ? 'rejection' : 'exception';
502+
const fnType = stackStartFn.name === 'rejects' ? 'rejection' : 'exception';
489503
innerFail({
490504
actual: undefined,
491505
expected: error,
@@ -510,7 +524,8 @@ function expectsNoError(stackStartFn, actual, error, message) {
510524

511525
if (!error || expectedException(actual, error)) {
512526
const details = message ? `: ${message}` : '.';
513-
const fnType = stackStartFn === doesNotReject ? 'rejection' : 'exception';
527+
const fnType = stackStartFn.name === 'doesNotReject' ?
528+
'rejection' : 'exception';
514529
innerFail({
515530
actual,
516531
expected: error,
@@ -523,29 +538,21 @@ function expectsNoError(stackStartFn, actual, error, message) {
523538
throw actual;
524539
}
525540

526-
function throws(block, ...args) {
541+
assert.throws = function throws(block, ...args) {
527542
expectsError(throws, getActual(block), ...args);
528-
}
529-
530-
assert.throws = throws;
543+
};
531544

532-
async function rejects(block, ...args) {
545+
assert.rejects = async function rejects(block, ...args) {
533546
expectsError(rejects, await waitForActual(block), ...args);
534-
}
535-
536-
assert.rejects = rejects;
547+
};
537548

538-
function doesNotThrow(block, ...args) {
549+
assert.doesNotThrow = function doesNotThrow(block, ...args) {
539550
expectsNoError(doesNotThrow, getActual(block), ...args);
540-
}
541-
542-
assert.doesNotThrow = doesNotThrow;
551+
};
543552

544-
async function doesNotReject(block, ...args) {
553+
assert.doesNotReject = async function doesNotReject(block, ...args) {
545554
expectsNoError(doesNotReject, await waitForActual(block), ...args);
546-
}
547-
548-
assert.doesNotReject = doesNotReject;
555+
};
549556

550557
assert.ifError = function ifError(err) {
551558
if (err !== null && err !== undefined) {

test/parallel/test-assert-async.js

Lines changed: 99 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,116 @@
11
'use strict';
22
const common = require('../common');
33
const assert = require('assert');
4-
const { promisify } = require('util');
5-
const wait = promisify(setTimeout);
6-
7-
/* eslint-disable prefer-common-expectserror, no-restricted-properties */
84

95
// Test assert.rejects() and assert.doesNotReject() by checking their
106
// expected output and by verifying that they do not work sync
117

128
common.crashOnUnhandledRejection();
139

14-
(async () => {
15-
await assert.rejects(
16-
async () => assert.fail(),
17-
common.expectsError({
18-
code: 'ERR_ASSERTION',
19-
type: assert.AssertionError,
20-
message: 'Failed'
21-
})
22-
);
10+
// Run all tests in parallel and check their outcome at the end.
11+
const promises = [];
2312

24-
await assert.doesNotReject(() => {});
13+
// Check `assert.rejects`.
14+
{
15+
const rejectingFn = async () => assert.fail();
16+
const errObj = {
17+
code: 'ERR_ASSERTION',
18+
name: 'AssertionError [ERR_ASSERTION]',
19+
message: 'Failed'
20+
};
21+
// `assert.rejects` accepts a function or a promise as first argument.
22+
promises.push(assert.rejects(rejectingFn, errObj));
23+
promises.push(assert.rejects(rejectingFn(), errObj));
24+
}
2525

26-
{
27-
const promise = assert.doesNotReject(async () => {
28-
await wait(1);
29-
throw new Error();
30-
});
31-
await assert.rejects(
32-
() => promise,
33-
(err) => {
34-
assert(err instanceof assert.AssertionError,
35-
`${err.name} is not instance of AssertionError`);
36-
assert.strictEqual(err.code, 'ERR_ASSERTION');
37-
assert.strictEqual(err.message,
38-
'Got unwanted rejection.\nActual message: ""');
39-
assert.strictEqual(err.operator, 'doesNotReject');
40-
assert.ok(!err.stack.includes('at Function.doesNotReject'));
41-
return true;
42-
}
43-
);
44-
}
26+
{
27+
const handler = (err) => {
28+
assert(err instanceof assert.AssertionError,
29+
`${err.name} is not instance of AssertionError`);
30+
assert.strictEqual(err.code, 'ERR_ASSERTION');
31+
assert.strictEqual(err.message,
32+
'Missing expected rejection (handler).');
33+
assert.strictEqual(err.operator, 'rejects');
34+
assert.ok(!err.stack.includes('at Function.rejects'));
35+
return true;
36+
};
37+
38+
let promise = assert.rejects(async () => {}, handler);
39+
promises.push(assert.rejects(promise, handler));
40+
41+
promise = assert.rejects(() => {}, handler);
42+
promises.push(assert.rejects(promise, handler));
43+
44+
promise = assert.rejects(Promise.resolve(), handler);
45+
promises.push(assert.rejects(promise, handler));
46+
}
47+
48+
{
49+
const THROWN_ERROR = new Error();
50+
51+
promises.push(assert.rejects(() => {
52+
throw THROWN_ERROR;
53+
}).catch(common.mustCall((err) => {
54+
assert.strictEqual(err, THROWN_ERROR);
55+
})));
56+
}
4557

58+
promises.push(assert.rejects(
59+
assert.rejects('fail', {}),
4660
{
47-
const promise = assert.rejects(() => {});
48-
await assert.rejects(
49-
() => promise,
50-
(err) => {
51-
assert(err instanceof assert.AssertionError,
52-
`${err.name} is not instance of AssertionError`);
53-
assert.strictEqual(err.code, 'ERR_ASSERTION');
54-
assert(/^Missing expected rejection\.$/.test(err.message));
55-
assert.strictEqual(err.operator, 'rejects');
56-
assert.ok(!err.stack.includes('at Function.rejects'));
57-
return true;
58-
}
59-
);
61+
code: 'ERR_INVALID_ARG_TYPE',
62+
message: 'The "block" argument must be one of type ' +
63+
'Function or Promise. Received type string'
6064
}
65+
));
6166

67+
// Check `assert.doesNotReject`.
68+
{
69+
// `assert.doesNotReject` accepts a function or a promise as first argument.
70+
promises.push(assert.doesNotReject(() => {}));
71+
promises.push(assert.doesNotReject(async () => {}));
72+
promises.push(assert.doesNotReject(Promise.resolve()));
73+
}
74+
75+
{
76+
const handler1 = (err) => {
77+
assert(err instanceof assert.AssertionError,
78+
`${err.name} is not instance of AssertionError`);
79+
assert.strictEqual(err.code, 'ERR_ASSERTION');
80+
assert.strictEqual(err.message, 'Failed');
81+
return true;
82+
};
83+
const handler2 = (err) => {
84+
assert(err instanceof assert.AssertionError,
85+
`${err.name} is not instance of AssertionError`);
86+
assert.strictEqual(err.code, 'ERR_ASSERTION');
87+
assert.strictEqual(err.message,
88+
'Got unwanted rejection.\nActual message: "Failed"');
89+
assert.strictEqual(err.operator, 'doesNotReject');
90+
assert.ok(!err.stack.includes('at Function.doesNotReject'));
91+
return true;
92+
};
93+
94+
const rejectingFn = async () => assert.fail();
95+
96+
let promise = assert.doesNotReject(rejectingFn, handler1);
97+
promises.push(assert.rejects(promise, handler2));
98+
99+
promise = assert.doesNotReject(rejectingFn(), handler1);
100+
promises.push(assert.rejects(promise, handler2));
101+
102+
promise = assert.doesNotReject(() => assert.fail(), common.mustNotCall());
103+
promises.push(assert.rejects(promise, handler1));
104+
}
105+
106+
promises.push(assert.rejects(
107+
assert.doesNotReject(123),
62108
{
63-
const THROWN_ERROR = new Error();
64-
65-
await assert.rejects(() => {
66-
throw THROWN_ERROR;
67-
}).then(common.mustNotCall())
68-
.catch(
69-
common.mustCall((err) => {
70-
assert.strictEqual(err, THROWN_ERROR);
71-
})
72-
);
109+
code: 'ERR_INVALID_ARG_TYPE',
110+
message: 'The "block" argument must be one of type ' +
111+
'Function or Promise. Received type number'
73112
}
74-
})().then(common.mustCall());
113+
));
114+
115+
// Make sure all async code gets properly executed.
116+
Promise.all(promises).then(common.mustCall());

0 commit comments

Comments
 (0)