Skip to content

Commit

Permalink
Allow throws / throwsAsync to work with any value, not just errors
Browse files Browse the repository at this point in the history
Fixes #2517.

Co-authored-by: Mark Wubben <mark@novemberborn.net>
  • Loading branch information
adiSuper94 and novemberborn committed Sep 11, 2023
1 parent 4c5b469 commit e81f413
Show file tree
Hide file tree
Showing 16 changed files with 141 additions and 39 deletions.
6 changes: 4 additions & 2 deletions docs/03-assertions.md
Expand Up @@ -172,10 +172,11 @@ Finally, this returns a boolean indicating whether the assertion passed.

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

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.
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.

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

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

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.

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.
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.

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

* `any`: a boolean only available in AVA 6, if `true` then the thrown value does not need to be an error. Defaults to `false`
* `instanceOf`: a constructor, the thrown error must be an instance of
* `is`: the thrown error must be strictly equal to `expectation.is`
* `message`: the following types are valid:
Expand Down
7 changes: 6 additions & 1 deletion docs/08-common-pitfalls.md
Expand Up @@ -14,7 +14,12 @@ Note that the following is not a native error:
const error = Object.create(Error.prototype);
```

This can be surprising, since `error instanceof Error` returns `true`.
This can be surprising, since `error instanceof Error` returns `true`. You can set `any: true` in the expectations to handle these values:

```js
const error = Object.create(Error.prototype);
t.throws(() => { throw error }, {any: true});
```

## AVA in Docker

Expand Down
13 changes: 11 additions & 2 deletions lib/assert.js
Expand Up @@ -127,13 +127,21 @@ function validateExpectations(assertion, expectations, numberArgs) { // eslint-d
});
}

if (Object.hasOwn(expectations, 'any') && typeof expectations.any !== 'boolean') {
throw new AssertionError(`The \`any\` property of the second argument to \`${assertion}\` must be a boolean`, {
assertion,
formattedDetails: [formatWithLabel('Called with:', expectations)],
});
}

for (const key of Object.keys(expectations)) {
switch (key) {
case 'instanceOf':
case 'is':
case 'message':
case 'name':
case 'code': {
case 'code':
case 'any': {
continue;
}

Expand All @@ -153,7 +161,8 @@ function validateExpectations(assertion, expectations, numberArgs) { // eslint-d
// Note: this function *must* throw exceptions, since it can be used
// as part of a pending assertion for promises.
function assertExpectations({actual, expectations, message, prefix, assertion, assertionStack}) {
if (!isNativeError(actual)) {
const allowThrowAnything = Object.hasOwn(expectations, 'any') && expectations.any;
if (!isNativeError(actual) && !allowThrowAnything) {
throw new AssertionError(message, {
assertion,
assertionStack,
Expand Down
27 changes: 27 additions & 0 deletions test-tap/assert.js
Expand Up @@ -822,6 +822,11 @@ test('.throws()', gather(t => {
throw new Error('foo');
}));

// Passes when string is thrown, only when any is set to true.
passes(t, () => assertions.throws(() => {
throw 'foo'; // eslint-disable-line no-throw-literal
}, {any: true}));

// Passes because the correct error is thrown.
passes(t, () => {
const error = new Error('foo');
Expand Down Expand Up @@ -1023,9 +1028,19 @@ test('.throwsAsync()', gather(t => {
formattedDetails: [{label: 'Returned promise resolved with:', formatted: /'foo'/}],
});

// Fails because the function returned a promise that rejected, but not with an error.
throwsAsyncFails(t, () => assertions.throwsAsync(() => Promise.reject('foo')), { // eslint-disable-line prefer-promise-reject-errors
assertion: 't.throwsAsync()',
message: '',
formattedDetails: [{label: 'Returned promise rejected with exception that is not an error:', formatted: /'foo'/}],
});

// Passes because the promise was rejected with an error.
throwsAsyncPasses(t, () => assertions.throwsAsync(Promise.reject(new Error())));

// Passes because the promise was rejected with an with an non-error exception, & set `any` to true in expectation.
throwsAsyncPasses(t, () => assertions.throwsAsync(Promise.reject('foo'), {any: true})); // eslint-disable-line prefer-promise-reject-errors

// Passes because the function returned a promise rejected with an error.
throwsAsyncPasses(t, () => assertions.throwsAsync(() => Promise.reject(new Error())));

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

failsWith(t, () => assertions.throws(() => {}, {any: {}}), {
assertion: 't.throws()',
message: 'The `any` property of the second argument to `t.throws()` must be a boolean',
formattedDetails: [{label: 'Called with:', formatted: /any: {}/}],
});

failsWith(t, () => assertions.throws(() => {}, {code: {}}), {
assertion: 't.throws()',
message: 'The `code` property of the second argument to `t.throws()` must be a string or number',
Expand Down Expand Up @@ -1204,6 +1225,12 @@ test('.throwsAsync() fails if passed a bad expectation', t => {
formattedDetails: [{label: 'Called with:', formatted: /\[]/}],
}, {expectBoolean: false});

failsWith(t, () => assertions.throwsAsync(() => {}, {any: {}}), {
assertion: 't.throwsAsync()',
message: 'The `any` property of the second argument to `t.throwsAsync()` must be a boolean',
formattedDetails: [{label: 'Called with:', formatted: /any: {}/}],
}, {expectBoolean: false});

failsWith(t, () => assertions.throwsAsync(() => {}, {code: {}}), {
assertion: 't.throwsAsync()',
message: 'The `code` property of the second argument to `t.throwsAsync()` must be a string or number',
Expand Down
2 changes: 1 addition & 1 deletion test-tap/reporters/tap.failfast.v16.log
Expand Up @@ -5,7 +5,7 @@ not ok 1 - a › fails
name: AssertionError
assertion: t.fail()
message: Test failed via `t.fail()`
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
...
---tty-stream-chunk-separator

Expand Down
2 changes: 1 addition & 1 deletion test-tap/reporters/tap.failfast.v18.log
Expand Up @@ -5,7 +5,7 @@ not ok 1 - a › fails
name: AssertionError
assertion: t.fail()
message: Test failed via `t.fail()`
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
...
---tty-stream-chunk-separator

Expand Down
2 changes: 1 addition & 1 deletion test-tap/reporters/tap.failfast.v20.log
Expand Up @@ -5,7 +5,7 @@ not ok 1 - a › fails
name: AssertionError
assertion: t.fail()
message: Test failed via `t.fail()`
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
...
---tty-stream-chunk-separator

Expand Down
2 changes: 1 addition & 1 deletion test-tap/reporters/tap.failfast2.v16.log
Expand Up @@ -5,7 +5,7 @@ not ok 1 - a › fails
name: AssertionError
assertion: t.fail()
message: Test failed via `t.fail()`
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
...
---tty-stream-chunk-separator
# 1 test remaining in a.cjs
Expand Down
2 changes: 1 addition & 1 deletion test-tap/reporters/tap.failfast2.v18.log
Expand Up @@ -5,7 +5,7 @@ not ok 1 - a › fails
name: AssertionError
assertion: t.fail()
message: Test failed via `t.fail()`
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
...
---tty-stream-chunk-separator
# 1 test remaining in a.cjs
Expand Down
2 changes: 1 addition & 1 deletion test-tap/reporters/tap.failfast2.v20.log
Expand Up @@ -5,7 +5,7 @@ not ok 1 - a › fails
name: AssertionError
assertion: t.fail()
message: Test failed via `t.fail()`
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
...
---tty-stream-chunk-separator
# 1 test remaining in a.cjs
Expand Down
12 changes: 6 additions & 6 deletions test-tap/reporters/tap.regular.v16.log
Expand Up @@ -30,7 +30,7 @@ not ok 3 - nested-objects › format with max depth 4
+ },
}
message: ''
at: 'ExecutionContext.deepEqual (/lib/assert.js:351:9)'
at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)'
...
---tty-stream-chunk-separator
not ok 4 - nested-objects › format like with max depth 4
Expand All @@ -48,7 +48,7 @@ not ok 4 - nested-objects › format like with max depth 4
},
}
message: ''
at: 'ExecutionContext.like (/lib/assert.js:413:9)'
at: 'ExecutionContext.like (/lib/assert.js:422:9)'
...
---tty-stream-chunk-separator
# output-in-hook › before hook
Expand All @@ -72,7 +72,7 @@ not ok 6 - output-in-hook › failing test
name: AssertionError
assertion: t.fail()
message: Test failed via `t.fail()`
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
...
---tty-stream-chunk-separator
# output-in-hook › afterEach hook for passing test
Expand Down Expand Up @@ -102,7 +102,7 @@ not ok 10 - test › fails
name: AssertionError
assertion: t.fail()
message: Test failed via `t.fail()`
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
...
---tty-stream-chunk-separator
ok 11 - test › known failure
Expand All @@ -123,7 +123,7 @@ not ok 13 - test › logs
name: AssertionError
assertion: t.fail()
message: Test failed via `t.fail()`
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
...
---tty-stream-chunk-separator
not ok 14 - test › formatted
Expand All @@ -135,7 +135,7 @@ not ok 14 - test › formatted
- 'foo'
+ 'bar'
message: ''
at: 'ExecutionContext.deepEqual (/lib/assert.js:351:9)'
at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)'
...
---tty-stream-chunk-separator
not ok 15 - test › implementation throws non-error
Expand Down
12 changes: 6 additions & 6 deletions test-tap/reporters/tap.regular.v18.log
Expand Up @@ -30,7 +30,7 @@ not ok 3 - nested-objects › format with max depth 4
+ },
}
message: ''
at: 'ExecutionContext.deepEqual (/lib/assert.js:351:9)'
at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)'
...
---tty-stream-chunk-separator
not ok 4 - nested-objects › format like with max depth 4
Expand All @@ -48,7 +48,7 @@ not ok 4 - nested-objects › format like with max depth 4
},
}
message: ''
at: 'ExecutionContext.like (/lib/assert.js:413:9)'
at: 'ExecutionContext.like (/lib/assert.js:422:9)'
...
---tty-stream-chunk-separator
# output-in-hook › before hook
Expand All @@ -72,7 +72,7 @@ not ok 6 - output-in-hook › failing test
name: AssertionError
assertion: t.fail()
message: Test failed via `t.fail()`
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
...
---tty-stream-chunk-separator
# output-in-hook › afterEach hook for passing test
Expand Down Expand Up @@ -102,7 +102,7 @@ not ok 10 - test › fails
name: AssertionError
assertion: t.fail()
message: Test failed via `t.fail()`
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
...
---tty-stream-chunk-separator
ok 11 - test › known failure
Expand All @@ -123,7 +123,7 @@ not ok 13 - test › logs
name: AssertionError
assertion: t.fail()
message: Test failed via `t.fail()`
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
...
---tty-stream-chunk-separator
not ok 14 - test › formatted
Expand All @@ -135,7 +135,7 @@ not ok 14 - test › formatted
- 'foo'
+ 'bar'
message: ''
at: 'ExecutionContext.deepEqual (/lib/assert.js:351:9)'
at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)'
...
---tty-stream-chunk-separator
not ok 15 - test › implementation throws non-error
Expand Down
12 changes: 6 additions & 6 deletions test-tap/reporters/tap.regular.v20.log
Expand Up @@ -30,7 +30,7 @@ not ok 3 - nested-objects › format with max depth 4
+ },
}
message: ''
at: 'ExecutionContext.deepEqual (/lib/assert.js:351:9)'
at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)'
...
---tty-stream-chunk-separator
not ok 4 - nested-objects › format like with max depth 4
Expand All @@ -48,7 +48,7 @@ not ok 4 - nested-objects › format like with max depth 4
},
}
message: ''
at: 'ExecutionContext.like (/lib/assert.js:413:9)'
at: 'ExecutionContext.like (/lib/assert.js:422:9)'
...
---tty-stream-chunk-separator
# output-in-hook › before hook
Expand All @@ -72,7 +72,7 @@ not ok 6 - output-in-hook › failing test
name: AssertionError
assertion: t.fail()
message: Test failed via `t.fail()`
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
...
---tty-stream-chunk-separator
# output-in-hook › afterEach hook for passing test
Expand Down Expand Up @@ -102,7 +102,7 @@ not ok 10 - test › fails
name: AssertionError
assertion: t.fail()
message: Test failed via `t.fail()`
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
...
---tty-stream-chunk-separator
ok 11 - test › known failure
Expand All @@ -123,7 +123,7 @@ not ok 13 - test › logs
name: AssertionError
assertion: t.fail()
message: Test failed via `t.fail()`
at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
...
---tty-stream-chunk-separator
not ok 14 - test › formatted
Expand All @@ -135,7 +135,7 @@ not ok 14 - test › formatted
- 'foo'
+ 'bar'
message: ''
at: 'ExecutionContext.deepEqual (/lib/assert.js:351:9)'
at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)'
...
---tty-stream-chunk-separator
not ok 15 - test › implementation throws non-error
Expand Down
10 changes: 9 additions & 1 deletion test-types/import-in-cts/throws.cts
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import test from '../../entrypoints/main.cjs';
import {expectType} from 'tsd';
import {expectError, expectType} from 'tsd';

class CustomError extends Error {
foo: string;
Expand All @@ -23,6 +23,10 @@ test('throws', t => {
expectType<CustomError | undefined>(error4);
const error5 = t.throws(() => {}, {instanceOf: CustomError, is: new CustomError()});
expectType<CustomError | undefined>(error5);
const error6 = t.throws(() => { throw 'foo' }, {any: true});
expectType<unknown>(error6);
// @ts-expect-error TS2769
expectError(t.throws(() => { throw 'foo' }, {instanceOf: String, is: 'foo'}));
});

test('throwsAsync', async t => {
Expand All @@ -38,4 +42,8 @@ test('throwsAsync', async t => {
expectType<CustomError | undefined>(error4);
const error5 = await t.throwsAsync(async () => {}, {instanceOf: CustomError, is: new CustomError()});
expectType<CustomError | undefined>(error5);
const error6 = await t.throwsAsync(async () => { throw 'foo' }, {any: true});
expectType<unknown>(error6);
// @ts-expect-error TS2769
expectError(t.throwsAsync(async () => { throw 'foo' }, {instanceOf: String, is: 'foo'}));
});

0 comments on commit e81f413

Please sign in to comment.