-
-
Notifications
You must be signed in to change notification settings - Fork 702
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Throw using checkError module #683
Conversation
Hey @lucasfcosta this is looking great. I've created a check-error repo, if you could please make a PR putting the util js in there, that'd be aaaweeesome. Then we can rework this PR to depend on that lib 😄 |
Ah, of course @keithamus! |
Wow! This was quite an undertaking. Great work :D I have a few questions which I'll add line comments for in a moment. I also noticed that the issue mentioned in #551 still persists: expect(function() { throw new Error(); }).to.throw(Error, 'hello'); // fails
expect(function() { throw new Error(); }).to.not.throw(Error, 'hello'); // fails too Is this the desired behavior? |
* @param {String} message _optional_ | ||
* @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error#Error_types | ||
* @returns error for chaining (null if no error) | ||
* @namespace BDD | ||
* @api public | ||
*/ | ||
|
||
function assertThrows (constructor, errMsg, msg) { | ||
function assertThrows (errorLike, errMatcher, msg) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not crazy about the terms errorLike
and errMatcher
, partially because one is "error" and the other "err", and partially because I feel some context is lost with "errMatcher".
What do you think about errLike
and errMsgMatcher
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems awesome!
It really looks semantically better, thanks for the insight.
Hello everyone, This is the code I currently have: function assertThrows (errorLike, errMsgMatcher, msg) {
if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
var negate = flag(this, 'negate');
new Assertion(obj, msg).is.a('function');
if (errorLike instanceof RegExp || typeof errorLike === 'string') {
errMsgMatcher = errorLike;
errorLike = null;
}
var caughtErr;
try {
obj();
} catch (err) {
caughtErr = err;
}
// If we have the negate flag enabled and at least one valid argument it means we do expect an error
// but we want it to match a given set of criteria
var everyArgIsUndefined = errorLike === undefined && errMsgMatcher === undefined;
// Checking if error was thrown
if (everyArgIsUndefined || !everyArgIsUndefined && !negate) {
// We need this to display results correctly according to their types
var errorLikeString = 'an error';
if (errorLike instanceof Error) {
errorLikeString = '#{exp}';
} else if (errorLike) {
errorLikeString = errorLike.name;
}
this.assert(
caughtErr
, 'expected #{this} to throw ' + errorLikeString
, 'expected #{this} to not throw an error but #{act} was thrown'
, errorLike && errorLike.toString()
, (caughtErr instanceof Error ?
caughtErr.toString() : (typeof caughtErr === 'string' ? caughtErr : caughtErr && caughtErr.name))
);
}
// If we've got the negate flag enabled and both args, we should only fail if both aren't compatible
// See Issue #551@GitHub
var everyArgIsDefined = errorLike && errMsgMatcher;
var constructorError;
var messageError;
if (errorLike && caughtErr) {
// We should compare instances only if `errorLike` is an instance of `Error`
if (errorLike instanceof Error) {
this.assert(
_.checkError.compatibleInstance(caughtErr, errorLike)
, 'expected #{this} to throw #{exp} but #{act} was thrown'
, 'expected #{this} to not throw #{exp}' + (caughtErr && !negate ? ' but #{act} was thrown' : '')
, errorLike.toString()
, caughtErr.toString()
);
}
try {
this.assert(
_.checkError.compatibleConstructor(caughtErr, errorLike)
, 'expected #{this} to throw #{exp} but #{act} was thrown'
, 'expected #{this} to not throw #{exp}' + (caughtErr ? ' but #{act} was thrown' : '')
, (errorLike instanceof Error ? errorLike.toString() : errorLike && _.checkError.getConstructorName(errorLike))
, (caughtErr instanceof Error ? caughtErr.toString() : caughtErr && _.checkError.getConstructorName(caughtErr))
);
} catch(e) {
if (everyArgIsDefined && negate) {
constructorError = e;
} else {
throw e;
}
}
}
if (errMsgMatcher) {
// Here we check compatible messages
var placeholder = 'including';
if (errMsgMatcher instanceof RegExp) {
placeholder = 'matching'
}
try {
this.assert(
_.checkError.compatibleMessage(caughtErr, errMsgMatcher)
, 'expected #{this} to throw error ' + placeholder + ' #{exp} but got #{act}'
, 'expected #{this} to throw error not ' + placeholder + ' #{exp}'
, errMsgMatcher
, _.checkError.getMessage(caughtErr)
);
} catch(e) {
if (everyArgIsDefined && negate) {
messageError = e;
} else {
throw e;
}
}
}
if (everyArgIsDefined && negate) {
if (constructorError && messageError) {
throw constructorError;
}
}
flag(this, 'object', caughtErr);
}; This code passes every test, including: expect(function() { throw new Error(); }).to.throw(Error, 'hello'); // this should fail
expect(function() { throw new Error(); }).to.not.throw(Error, 'hello'); // this should pass The problem is: when running, for example, Also, even if I do it, we will still have to change tests, since some of them use So, what do you guys think? Should I |
What if we performed all of our validations near the top and stored the results in booleans? For example, we already do this here: var caughtErr;
try {
obj();
} catch (err) {
caughtErr = err;
} But what if we immediately went on to do this too, prior to any assertions: var hasCompatibleMessage = _.checkError.compatibleMessage(caughtErr, errMatcher)
, hasCompatibleInstance = _.checkError.compatibleInstance(caughtErr, errorLike)
, hasCompatibleConstructor = _.checkError.compatibleConstructor(caughtErr, errorLike); Would that give us more freedom to organize the assertions and make sure the best error message is thrown on a failure (especially for compound tests)? It might also allow some of the |
Thanks for the suggestion, @meeber! 😄 Unfortunately I'm not sure this would be a very good idea, because we would still need some Also, doing that prior to the assertions themselves would require us to add logic which is too tightly related to the And after all, the main problem here still persists: I'll explore the code some more and see if I can find another solution for this, thanks again for sharing your thoughts, it's been great to have such a good reviewer 😄 Edit: I've just pushed a new commit with the latest working version and new tests. It throws a |
0b487d3
to
7eed9ab
Compare
You're right! We definitely don't want any throw-specific logic in the helper functions, and we'd end up doing many of the same I think there may still be some value in lifting the Before I ramble any further, lemme see if I can cook up an example to express my thoughts in code :D |
Okay, after playing around with the code, thinking about the problem, staring at my cat, and eating a chicken sammich, here's my analysis: The reason this is so tricksy is because the negate logic is fundamentally different than the default logic. With the default logic, you can proceed assertion-by-assertion and short-circuit the function as soon as an assertion fails. Conversely, with the negate logic, you must perform all relevant assertions ("relevant" being based on which combination of errLike and errMsgMatcher is provided); you can never short-circuit. Because the default and negate logic are so different, you have three options:
There are issues with every option. Traditionally this function has gone with the first. I think that worked okay until trying to fix that one bug with the Of course, refactoring to use the 2nd or 3rd option is made a bit trickier by having to respect the current failure message precedence for backward compatibility purposes, but only a minor hurdle I think. |
Thanks for the help @meeber! The issue with the 3rd option is that it will create code repetition, because we've got to do some of the same things for both cases ( I'm also not a big fan of having to rethrow errors as we're currently doing, so my first goal on the next refactor will be to find a clean way of doing that. I will take some time this week for a new full refactor of You've been very helpful, thanks! 😄 Oh, and just in case you want to stare at cats other than yours, here you go, hahaha |
You've created a portal to eternal bliss. Well done :D I agree, the 3rd option likely isn't viable due to code repetition. An alternative to the current approach that avoids try/catch is to start with try {
this.assert(
_.checkError.compatibleConstructor(caughtErr, errorLike)
, 'expected #{this} to throw #{exp} but #{act} was thrown'
, 'expected #{this} to not throw #{exp}' + (caughtErr ? ' but #{act} was thrown' : '')
, (errorLike instanceof Error ? errorLike.toString() : errorLike && _.checkError.getConstructorName(errorLike))
, (caughtErr instanceof Error ? caughtErr.toString() : caughtErr && _.checkError.getConstructorName(caughtErr))
);
} catch(e) {
if (everyArgIsDefined && negate) {
constructorError = e;
} else {
throw e;
}
} To something like this: if (!_.checkError.compatibleConstructor(caughtErr, errorLike)) {
this.assert(
, false
, 'expected #{this} to throw #{exp} but #{act} was thrown'
, 'expected #{this} to not throw #{exp}' + (caughtErr ? ' but #{act} was thrown' : '')
, (errorLike instanceof Error ? errorLike.toString() : errorLike && _.checkError.getConstructorName(errorLike))
, (caughtErr instanceof Error ? caughtErr.toString() : caughtErr && _.checkError.getConstructorName(caughtErr))
);
perfectMatch = false;
} The non-negate behavior remains unchanged: If a compatibility check returns But the negate behavior changes: If a compatibility check returns At the end of the function, if The only challenge becomes filling in the There are other ways to achieve the same thing I'm sure. What it boils down to is not allowing the assertions to short-circuit the logic for |
Good idea, @meeber. |
7eed9ab
to
aaf89eb
Compare
It's finally done! My brain was broken into so many pieces I can't even think straight anymore, that was an awesome puzzle! I made some minor tweaks to @meeber's ideas and I could get it working. I had to create two new variables to indicate whether a check has failed or not and then I would mark them with This was the most elegant way I could get it to work, please feel free to suggest improvements if you find room for any. I would also like to thank @meeber for the help with this PR, without you this wouldn't have been possible, you've done a great job buddy, thanks! 😄 Important:Currently the fail message is the same as if the constructor check had failed, if you guys want this to be fully BC we can't change it, because maybe (it's a remote possibility, but we can't discard it) someone is expecting an specific error message (the constructor fail message), so changing it could break someone's tests. What do you guys think about this? |
Well done, sir! LGTM. That was not an easy one. I concur with your approach to maintain backward compatibility for now; we can consider improving the error message for a future breaking change. Thanks for all your hard work :D |
Thanks @meeber! Well, so @keithamus can merge this if he's happy with it 😄 |
@lucasfcosta @meeber is anything outstanding on chaijs/check-error#1? We should merge that and release it as a module, so we can then rework this PR to use the external module 😄 |
@keithamus chaijs/check-error#1 is complete, it just has minor linting-related improvements for the linting task to pass without errors, but they're the same. I will refactor this to use the external module as soon as it's published. 😄 |
b7c9748
to
46e56aa
Compare
The build is failing because the I have already:
|
46e56aa
to
e766ac3
Compare
49b00db
to
b9436a9
Compare
@meeber Done 🎉 🎉 |
@lucasfcosta Amazing job! LGTM! Where are you going on vacation to celebrate?? |
Thanks @meeber. There's two months left until I get my vacation, that's too much time to wait, let's throw a commit party on github 😄 |
@lucasfcosta I dunno that's more than a little terrifying :D Someone should definitely double-check this but I think we'll be able to close the following once this gets merged: #470 #551 #596 #634 #635 It doesn't seem to close #430, #436, or #559, but I'm not sure they're using a valid form of the It doesn't seem to close #355 either; stack trace still doesn't include original error frame. |
I think we can close all of those except #355. Here are the reasons for the ones you were not sure:
|
@lucasfcosta Thanks for double checking! I agree with your assessment. You might be setting the world record for most issues and PRs closed by a single commit. |
@keithamus I forgot to ping you on this. Sorreh! |
pester pester @keithamus |
@lucasfcosta Oh hey I just thought of something. We should add check-error to README.md under "Related Projects". Also!! I saw you ask a question on a youtube video!! :D |
@meeber Yes we should! PS.: Yes I did it! MPJ rocks, hahahaha, I love his channel 😄 |
Sorry for taking so long on this one. LGTM! |
@keithamus We all get plenty busy sometimes, don't worry about it Keith. Thanks everyone for your support 🎉 🎉 🎉 🎉 |
One last hurdle: Looks like a couple IE tests are failing. |
@meeber I'll check this out after lunch, that's strange since |
Just updating everyone: Today I'm gonna take some time to:
We will also need some more time to investigate and fix #355 in the future. |
Hello everyone!
This one is pretty big and we've got plenty of special cases at the
throws
assertion, so be careful when reviewing 😄This one aims to solve an issue for the 4.0.0 release (as stated on our Roadmap #457) and finish #470 implementation. I thought that before moving
checkError
to a repository of its own it would be nice to have it reviewed and merged by more people here and only then we would move it to a new repo, but I'll do what you guys think it's better, this is just my opinion, please let me know if you disagree 😉This PR includes:
checkError
module including DocStrings,compatibleInstance
,compatibleMessage
,compatibleConstructor
,getConstructorName
andgetMessage
checkError
functionsthrows
assertion in order for it to usecheckError
I couldn't make the
throws
assertion more simple than this, because we've got lots of edge/special cases, so this is the most generic and clean code I could get to while maintaining BC. If any of you can think of a better way to implement this please share your idea, it will certainly be welcome 😃I look forward to hearing your feedback 👍
EDIT: This will be added to the check-error repo as requested by @keithamus.
I'll write the complete repo structure and submit a PR there as soon as it's done.
Please don't merge this since it requires changes to use the external module instead of the local one.