ava: Simplify t.throws()
This is my proposal for resolving #661. In short I want to stop deferring to https://nodejs.org/api/assert.html#assert_assert_throws_block_error_message and remove various parameter overloads that are currently supported by the assertion.
Here’s what I’d like to support:
t.throws(fn) // Throws an Error (or subclass thereof)
t.throws(fn, SyntaxError) // Throws a SyntaxError (or subclass thereof)
t.throws(fn, 'string') // Throws an Error (or subclass thereof), whose .message === 'string'
t.throws(fn, /regexp/) // Throws an Error (or subclass thereof), whose /regexp/.test(err.message) === true
If you need to test the errors class and message you should use the t.throws
return value:
const err = t.throws(fn, SyntaxError)
t.true(err.message === 'expecting integer')
Passing anything other than undefined
, functions, strings or regular expressions as the second argument results in an TypeError
from t.throws()
itself.
The first argument is allowed to be a promise or observable, as is any return value from the fn
call. Of course this makes t.throws
asynchronous, so users may need to do:
const err = await t.throws(fn)
Questions:
-
Does
t.throws(fn, Constructor)
require the thrown value to be anError
? For example:t.throws(() => { throw 'foo' }, String) // Is this allowed?
-
Should
t.throws(fn, 'string')
andt.throws(fn, /regexp/)
be supported at all, or would it be preferable to dereference the error and then use another assertion?const err = t.throws(fn) t.true(err.message === 'expecting integer')
-
Is there a need for
t.throws(fn, SyntaxError, 'string')
andt.throws(fn, SyntaxError, /regexp/)
-
Does anybody have experience with / examples of asserting
Error
instances across contexts?
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Comments: 25 (18 by maintainers)
Commits related to this issue
- Implement `t.throws()` and `t.notThrows()` ourselves Remove `core-assert` dependency. When passed a function as the second argument, `t.throws()` now assumes its a constructor. This removes support f... — committed to avajs/ava by novemberborn 6 years ago
- Implement `t.throws()` and `t.notThrows()` ourselves Remove `core-assert` dependency. When passed a function as the second argument, `t.throws()` now assumes its a constructor. This removes support f... — committed to avajs/ava by novemberborn 6 years ago
- Implement `t.throws()` and `t.notThrows()` ourselves Remove `core-assert` dependency. When passed a function as the second argument, `t.throws()` now assumes its a constructor. This removes support f... — committed to avajs/ava by novemberborn 6 years ago
- Support expectation object in t.throws() Fixes #1047. Fixes #1676. A combination of the following expectations is supported: ```js t.throws(fn, {of: SyntaxError}) // err instanceof SyntaxError t.th... — committed to avajs/ava by novemberborn 6 years ago
- Implement `t.throws()` and `t.notThrows()` ourselves Remove `core-assert` dependency. When passed a function as the second argument, `t.throws()` now assumes its a constructor. This removes support f... — committed to avajs/ava by novemberborn 6 years ago
- Support expectation object in t.throws() Fixes #1047. Fixes #1676. A combination of the following expectations is supported: ```js t.throws(fn, {of: SyntaxError}) // err instanceof SyntaxError t.th... — committed to avajs/ava by novemberborn 6 years ago
- Reimplement t.throws() and t.notThrows() (#1704) Remove `core-assert` dependency. When passed a function as the second argument, `t.throws()` now assumes its a constructor. This removes support for... — committed to avajs/ava by novemberborn 6 years ago
Alternative proposal.
Make it
t.throws(fn, matchers, [message]);
, wherematchers
is an object of different matchers that all has to match. Matchers can be string or regex.This could easily be an addition to supporting string/regex/constructor in the second argument, for backwards compatibility.
For example:
Compared to the current way:
Possible matchers
constructor
,message
. Could also consider supportingname
for when you don’t have easy access to the constructor function. Maybe alsostack
if you want to ensure something is part of it, though I don’t think I’ve ever needed that.Benefits of this is that it makes everything explicit. No more wondering for users what
t.throws(() => foo(), 'Unicorn')
matches against - Is it the name, type, or message. There’s also benefit in being able to specify all the constraints directly instead of in separate calls later, like potential for a better diff.We should also have much better validation logic to prevent mistakes. For example, using
is-error-constructor
on theconstructor
matcher.Yes
It creates verbose code for common cases though. The most common case is wanting to ensure the error is the correct type and has the correct message. I think we should optimize for that.
@StoneCypher When we implement
t.throws()
in AVA directly we’ll make sure it explains why a thrown value still causes the assertion to fail.@AlexTes
I don’t personally need this feature much, but for sake of argument…
Say
myApp()
uses a database under the hood and potentially rejects with variousDatabaseError
s. You probably want to make assertions about failure cases formyApp()
working with the database, but the relevant error constructor does not live in your app and very well may not be exposed by the database driver library (it is uncommon to export all such classes).You still need to distinguish between different types of errors and, without the constructor,
name
is the preferred way to ID an error (should be more stable thanmessage
). Changes to these are arguably semver major, so you should probably test them. This is the best you can do without the constructor.@AlexTes
It’s usually better to compare errors using their
.name
.if (error instanceof MyError)
doesn’t necessarily work across VM contexts, for example. Libraries might also not bother exporting their error constructors.It’s currently implemented as comparing the message. We should do the same with the regular expression argument, IMO.
Historically the problem with the
throws
assertion is that it tries to do too many things, making it hard to understand what you’re actually testing. Running a regular expression against the string representation of an error is an example of that. I didn’t even know that’s how it worked!Personally I don’t think we should maintain backwards compatibility, given how confusing
throws
is.@AlexTes we can go with @sindresorhus’ proposal: https://github.com/avajs/ava/issues/1047#issuecomment-261483195
Match
constructor
by reference,name
by equality, andmessage
either by testing a regular expression or string equality.Yes that’s implied.
Oh no! Forgot to add that…
I think that rules out syntax like
t.throws(fn, SyntaxError, 'string')
, since that collides witht.throws(fn, SyntaxError, 'message')
. Padding it is just annoying (t.throws(fn, SyntaxError, undefined, 'message')
. Though you would need to padt.throws(fn, undefined, 'message')
. I think that’s OK though.Conclusions so far:
Yes.
We’ll support this interface.
Leaning towards not supporting this.
@vdemedes
@fearphage yea AVA just uses Node.js’ API, and it gets messy fast. Hence this proposal.