Skip to content

Commit e81f413

Browse files
Allow throws / throwsAsync to work with any value, not just errors
Fixes #2517. Co-authored-by: Mark Wubben <[email protected]>
1 parent 4c5b469 commit e81f413

16 files changed

+141
-39
lines changed

docs/03-assertions.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,11 @@ Finally, this returns a boolean indicating whether the assertion passed.
172172

173173
### `.throws(fn, expectation?, message?)`
174174

175-
Assert that an error is thrown. `fn` must be a function which should throw. The thrown value *must* be an error. It is returned so you can run more assertions against it. If the assertion fails then `undefined` is returned.
175+
Assert that an error is thrown. `fn` must be a function which should throw. By default, the thrown value *must* be an error. It is returned so you can run more assertions against it. If the assertion fails then `undefined` is returned.
176176

177177
`expectation` can be an object with one or more of the following properties:
178178

179+
* `any`: a boolean only available in AVA 6, if `true` then the thrown value does not need to be an error. Defaults to `false`
179180
* `instanceOf`: a constructor, the thrown error must be an instance of
180181
* `is`: the thrown error must be strictly equal to `expectation.is`
181182
* `message`: the following types are valid:
@@ -207,10 +208,11 @@ test('throws', t => {
207208

208209
Assert that an error is thrown. `thrower` can be an async function which should throw, or a promise that should reject. This assertion must be awaited.
209210

210-
The thrown value *must* be an error. It is returned so you can run more assertions against it. If the assertion fails then `undefined` is returned.
211+
By default, the thrown value *must* be an error. It is returned so you can run more assertions against it. If the assertion fails then `undefined` is returned.
211212

212213
`expectation` can be an object with one or more of the following properties:
213214

215+
* `any`: a boolean only available in AVA 6, if `true` then the thrown value does not need to be an error. Defaults to `false`
214216
* `instanceOf`: a constructor, the thrown error must be an instance of
215217
* `is`: the thrown error must be strictly equal to `expectation.is`
216218
* `message`: the following types are valid:

docs/08-common-pitfalls.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ Note that the following is not a native error:
1414
const error = Object.create(Error.prototype);
1515
```
1616

17-
This can be surprising, since `error instanceof Error` returns `true`.
17+
This can be surprising, since `error instanceof Error` returns `true`. You can set `any: true` in the expectations to handle these values:
18+
19+
```js
20+
const error = Object.create(Error.prototype);
21+
t.throws(() => { throw error }, {any: true});
22+
```
1823

1924
## AVA in Docker
2025

lib/assert.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,21 @@ function validateExpectations(assertion, expectations, numberArgs) { // eslint-d
127127
});
128128
}
129129

130+
if (Object.hasOwn(expectations, 'any') && typeof expectations.any !== 'boolean') {
131+
throw new AssertionError(`The \`any\` property of the second argument to \`${assertion}\` must be a boolean`, {
132+
assertion,
133+
formattedDetails: [formatWithLabel('Called with:', expectations)],
134+
});
135+
}
136+
130137
for (const key of Object.keys(expectations)) {
131138
switch (key) {
132139
case 'instanceOf':
133140
case 'is':
134141
case 'message':
135142
case 'name':
136-
case 'code': {
143+
case 'code':
144+
case 'any': {
137145
continue;
138146
}
139147

@@ -153,7 +161,8 @@ function validateExpectations(assertion, expectations, numberArgs) { // eslint-d
153161
// Note: this function *must* throw exceptions, since it can be used
154162
// as part of a pending assertion for promises.
155163
function assertExpectations({actual, expectations, message, prefix, assertion, assertionStack}) {
156-
if (!isNativeError(actual)) {
164+
const allowThrowAnything = Object.hasOwn(expectations, 'any') && expectations.any;
165+
if (!isNativeError(actual) && !allowThrowAnything) {
157166
throw new AssertionError(message, {
158167
assertion,
159168
assertionStack,

test-tap/assert.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,11 @@ test('.throws()', gather(t => {
822822
throw new Error('foo');
823823
}));
824824

825+
// Passes when string is thrown, only when any is set to true.
826+
passes(t, () => assertions.throws(() => {
827+
throw 'foo'; // eslint-disable-line no-throw-literal
828+
}, {any: true}));
829+
825830
// Passes because the correct error is thrown.
826831
passes(t, () => {
827832
const error = new Error('foo');
@@ -1023,9 +1028,19 @@ test('.throwsAsync()', gather(t => {
10231028
formattedDetails: [{label: 'Returned promise resolved with:', formatted: /'foo'/}],
10241029
});
10251030

1031+
// Fails because the function returned a promise that rejected, but not with an error.
1032+
throwsAsyncFails(t, () => assertions.throwsAsync(() => Promise.reject('foo')), { // eslint-disable-line prefer-promise-reject-errors
1033+
assertion: 't.throwsAsync()',
1034+
message: '',
1035+
formattedDetails: [{label: 'Returned promise rejected with exception that is not an error:', formatted: /'foo'/}],
1036+
});
1037+
10261038
// Passes because the promise was rejected with an error.
10271039
throwsAsyncPasses(t, () => assertions.throwsAsync(Promise.reject(new Error())));
10281040

1041+
// Passes because the promise was rejected with an with an non-error exception, & set `any` to true in expectation.
1042+
throwsAsyncPasses(t, () => assertions.throwsAsync(Promise.reject('foo'), {any: true})); // eslint-disable-line prefer-promise-reject-errors
1043+
10291044
// Passes because the function returned a promise rejected with an error.
10301045
throwsAsyncPasses(t, () => assertions.throwsAsync(() => Promise.reject(new Error())));
10311046

@@ -1134,6 +1149,12 @@ test('.throws() fails if passed a bad expectation', t => {
11341149
formattedDetails: [{label: 'Called with:', formatted: /\[]/}],
11351150
});
11361151

1152+
failsWith(t, () => assertions.throws(() => {}, {any: {}}), {
1153+
assertion: 't.throws()',
1154+
message: 'The `any` property of the second argument to `t.throws()` must be a boolean',
1155+
formattedDetails: [{label: 'Called with:', formatted: /any: {}/}],
1156+
});
1157+
11371158
failsWith(t, () => assertions.throws(() => {}, {code: {}}), {
11381159
assertion: 't.throws()',
11391160
message: 'The `code` property of the second argument to `t.throws()` must be a string or number',
@@ -1204,6 +1225,12 @@ test('.throwsAsync() fails if passed a bad expectation', t => {
12041225
formattedDetails: [{label: 'Called with:', formatted: /\[]/}],
12051226
}, {expectBoolean: false});
12061227

1228+
failsWith(t, () => assertions.throwsAsync(() => {}, {any: {}}), {
1229+
assertion: 't.throwsAsync()',
1230+
message: 'The `any` property of the second argument to `t.throwsAsync()` must be a boolean',
1231+
formattedDetails: [{label: 'Called with:', formatted: /any: {}/}],
1232+
}, {expectBoolean: false});
1233+
12071234
failsWith(t, () => assertions.throwsAsync(() => {}, {code: {}}), {
12081235
assertion: 't.throwsAsync()',
12091236
message: 'The `code` property of the second argument to `t.throwsAsync()` must be a string or number',

test-tap/reporters/tap.failfast.v16.log

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ not ok 1 - a › fails
55
name: AssertionError
66
assertion: t.fail()
77
message: Test failed via `t.fail()`
8-
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
8+
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
99
...
1010
---tty-stream-chunk-separator
1111

test-tap/reporters/tap.failfast.v18.log

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ not ok 1 - a › fails
55
name: AssertionError
66
assertion: t.fail()
77
message: Test failed via `t.fail()`
8-
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
8+
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
99
...
1010
---tty-stream-chunk-separator
1111

test-tap/reporters/tap.failfast.v20.log

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ not ok 1 - a › fails
55
name: AssertionError
66
assertion: t.fail()
77
message: Test failed via `t.fail()`
8-
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
8+
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
99
...
1010
---tty-stream-chunk-separator
1111

test-tap/reporters/tap.failfast2.v16.log

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ not ok 1 - a › fails
55
name: AssertionError
66
assertion: t.fail()
77
message: Test failed via `t.fail()`
8-
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
8+
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
99
...
1010
---tty-stream-chunk-separator
1111
# 1 test remaining in a.cjs

test-tap/reporters/tap.failfast2.v18.log

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ not ok 1 - a › fails
55
name: AssertionError
66
assertion: t.fail()
77
message: Test failed via `t.fail()`
8-
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
8+
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
99
...
1010
---tty-stream-chunk-separator
1111
# 1 test remaining in a.cjs

test-tap/reporters/tap.failfast2.v20.log

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ not ok 1 - a › fails
55
name: AssertionError
66
assertion: t.fail()
77
message: Test failed via `t.fail()`
8-
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
8+
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
99
...
1010
---tty-stream-chunk-separator
1111
# 1 test remaining in a.cjs

0 commit comments

Comments
 (0)