Skip to content
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

WIP: Overhaul .throw #1220

Closed
wants to merge 3 commits into from
Closed

WIP: Overhaul .throw #1220

wants to merge 3 commits into from

Conversation

meeber
Copy link
Contributor

@meeber meeber commented Dec 29, 2018

I've been thinking lately about chaijs/check-error#18. I think some of my decisions in that PR were wrong; in particular, dropping support for checking non-Error instances. Although only throwing instances of Error (including subclassed errors, like TypeError) is a best practice, it's not an important enough issue to justify enforcing that best practice on users in the form of a breaking change. However, there are other issues that are important enough to justify breaking changes:

  1. Eliminating the ambiguity of .throw('waffles'); currently it asserts that the target function throws either a string that contains "waffles", or an object that has a .message property that contains "waffles". Users typically have only one of those scenarios in mind when writing the assertion, and it's an unwelcome surprise for the assertion to pass given the other scenario.

  2. Eliminating the ambiguity of .not.throw(someCriteria); currently it asserts that the target function either doesn't throw, or it throws but the thrown value doesn't match one or more of the given criteria. Again, users typically have only one of those scenarios in mind when writing the assertion.

  3. Eliminating a reasonable-looking but actually-meaningless assertion from always passing; currently expect(myFn).to.throw silently passes, regardless of whether or not myFn throws, because .throw is only a method-based assertion. This is an unwelcome surprise for users of an assertion library that makes use of property-based assertions (e.g., .true). (I'm opposed to jumping to the opposite extreme of removing all property-based assertions from Chai; despite these types of problems, I think property-based assertions are a critical part of Chai's identity and appeal for many users.)

  4. Replacing the seldom-used .throw(ErrorInstance) behavior, which currently checks if the thrown value is strictly (===) equal to ErrorInstance, with more useful behavior of checking that the thrown value is deeply equal to ErrorInstance. This change was enabled by feat: change error comparison algorithm again deep-eql#59.

This PR demonstrates the changes I have in mind in the form of an update to the docs for .throw. I believe that implementing these changes would significantly simplify the code (and possibly eliminate the need for a separate check-error module altogether), as most of the complexity of the current implementation is caused by handling the ambiguity of the first two issues described above. It'd also lead to the creation of a similar .error assertion.

Questions:

  1. What are your thoughts on these changes in general?
  2. How do you feel about a major release for these changes (and to drop Node v4 and old IE support) prior to the release of anything related to the work being done in the new-chai repo?
  3. Are the changes suggested here compatible with the work being done in the new-chai repo?

@meeber meeber requested a review from a team as a code owner December 29, 2018 02:23
@codecov
Copy link

codecov bot commented Dec 29, 2018

Codecov Report

Merging #1220 into master will increase coverage by 0.11%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1220      +/-   ##
==========================================
+ Coverage   94.51%   94.63%   +0.11%     
==========================================
  Files          32       32              
  Lines        1676     1676              
  Branches      404      394      -10     
==========================================
+ Hits         1584     1586       +2     
+ Misses         92       90       -2
Impacted Files Coverage Δ
lib/chai/core/assertions.js 99.69% <100%> (+0.3%) ⬆️
lib/chai/utils/overwriteProperty.js 90.9% <0%> (-4.55%) ⬇️
lib/chai/utils/inspect.js 82.7% <0%> (+0.54%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1cededa...435b363. Read the comment docs.

*
* expect(badFn).to.throw;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting addition - but it is also one we tried very much in vain to do back in Chai@2 days. The main issue here is that property assertions that assert and also return a function to assert messes a lot of stuff up. There were some terrible nasty bugs that came from this.

Aside from my concerns of implementation complexity, I also don't think this is actually internally consistent with the property-vs-assertion rule we have: throw takes arguments, and so having it as a property sometimes, and other times not, is something that does not have existing precedent.

Further to your point:

I'm opposed to jumping to the opposite extreme of removing all property-based assertions from Chai; despite these types of problems, I think property-based assertions are a critical part of Chai's identity and appeal for many users.

I'm of the opposite mind set. For one, we see a lot of issues around implicitly passing tests because people often mistake function-assertion for a property-assertion, and get many bug reports about this. For another it makes the learning cliff a lot harder, as we're probably the only library in the users tool-belt that has this behaviour. If it were a common idiom I'd be more behind it, but it is not. Another reason I'd like to see this pattern disappear is that it makes plugin development more complex, both from the API complexity but also from a philosophical perspective where not every plugin strictly adheres to the property-vs-method rubric, which of course in turn decreases usability because Chai's own rules aren't consistent.

Currently, with chai@5 there exists no way to handle property assertions, although I plan to add them in but via an optional interface, and that is forced based on something we can introspect in code (e.g. property assertions are made from any unary assertions - i.e one that only takes only an actual and not any additional arguments).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My two main arguments against making property assertions "opt-in" in chai@5 are:

  1. The history & popularity of Chai undermine the effectiveness of dropping property-based assertions this late in the game. Chai has had 7 years worth of blog posts and tutorials published on the Internet with assertions like expect(blah).to.be.true, so if a user unknowingly follows one of those outdated tutorials in chai@5, they now have an assertion that looks legit and has increased legitimacy from being a style that's demonstrated across many tutorials but now in chai@5 passes no matter what.
  2. This type of bug report dropped drastically in chai@4 with the introduction of proxy protection. There's now only a couple of remaining assertions in Chai core that could be reasonably written such that they look legit but pass no matter what. expect(fn).to.throw; and expect('meow').to.be.a.string;. I'm not familiar with the implementation complexity and bugs that popped up when trying to fix this in chai@2; I guess I'd have to experience that pain myself to understand!

Good points about increased complexity for plugin authors, and consistency issues across both the Chai plugin ecosystem as well as JavaScript as a whole. I'm convinced that dropping property-based assertions would've been the right decision at the start of the project, but I'm not so sure it is now given Chai's history & popularity. That's a tough one.

* invokes the target function and asserts that an error is thrown that's an
* instance of that error constructor.
* When invoked with one argument, and it's a function (presumably a
* constructor), `.throw` invokes the target function and asserts that it
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: s/presumably/typically/. presumably suggests we don't know what it will be, whereas typically suggests that is the common pattern.

* constructor, and the second is a string or regular expression, `.throw`
* invokes the function and asserts that an error is thrown that fulfills both
* conditions as described above.
* When invoked with two arguments, and the first is a function (presumably a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same nit: s/presumably/typically/

*
* expect(goodFn).to.not.throw();
* expect(badFn).to.throw(String, 'salmon');
* expect(badFn).to.throw(String, /salmon/);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this 👍

* var badFn = function () { throw nonErrorObject; };
*
* expect(badFn).to.throw(NonErrorConstructor, 'salmon');
* expect(badFn).to.throw(new NonErrorConstructor('Illegal salmon!'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I am to understand the proposed algorithm, the only reason this does this is because the logic falls through to using deep-eql. We should mention that explicitly, if it is the case, as there's plenty of edge cases around deep-eql which might trip a user up when making this assertion.

BREAKING CHANGE: Drop support for versions of Node below v6, and
update the Travis config accordingly.
BREAKING CHANGE: Update the deep equality algorithm used to compare
`Error` objects.
@meeber meeber force-pushed the overhaul-throw branch 3 times, most recently from b1c9713 to 7f41fc9 Compare February 1, 2019 23:58
@meeber
Copy link
Contributor Author

meeber commented Feb 2, 2019

@chaijs/chai Here's what I have in mind for solving problems 1, 2, and 4 that I described in my original post, while simultaneously simplifying code. Other assertions (e.g., rejectedWith and the yet-to-be-created .error) could utilize the createErrCriteria and assertErrCriteria functions to simplify their logic as well.

This is still a draft, and I haven't updated the should or assert interface tests.

And here is what it'd look like if .throw could be used as a property-based assertion to solve problem 3. As @keithamus pointed out, there are several valid reasons not to use property-based assertions, but I don't think implementation complexity should be included in that list.

}

return cri;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be interested to see these functions move into https://github.com/chaijs/check-error where they can be tested for their surface area, and can be reviewed+merged+shipped easier there, thereby making this PR smaller.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that. createErrCriteria could probably be moved to https://github.com/chaijs/check-error, but it'd be the only function there, without any real non-Chai applicability. assertErrCriteria is using a bunch of other Chai assertions internally, so I don't think it can be cleanly moved to a separate module until v5 when assertions are better modularized.

@meeber
Copy link
Contributor Author

meeber commented May 25, 2019

I'm still not convinced this is the right implementation. Gonna close for now and revisit post v5 release.

@meeber meeber closed this May 25, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants