-
-
Notifications
You must be signed in to change notification settings - Fork 276
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
Let's change the default for RSpec/PredicateMatcher #919
Comments
We don't have a formal checklist for changing the defaults, but if I had to come up with it, it would be:
It would be really nice to harmonize the defaults with the style guide and to harmonize the style guide with what is really commonly used in those open-source apps. It looks like a task on its own, but before we've done that, we can follow that "simple" checklist. Would you like to grep through |
This comment was marked as resolved.
This comment was marked as resolved.
FWIW, I think |
Completely agree.
If we are talking about predicates - sure, but there are certain cases when it's better to be less strict about expectations.
The need can be implied by a weird interface or even some weirdness in the Ruby itself: 0.zero?
# => true
0.nonzero?
#=> nil
1.zero?
#=> false
1.nonzero?
#=> 1 https://metaredux.com/posts/2019/06/11/weird-ruby-zeroing-in-on-a-couple-of-numeric-predicates.html |
The "weirdness" of
Legitimate ones? I'll still need to see one be convinced 😸 |
Hold on for a second. What are the With the default, which is With So are you talking about this bug, or do you propose to retire/deprecate the Regarding |
The reason these are in rspec is because these matchers used to be called I don't think I needed a PR as an example of my point, but here it is nevertheless: rspec/rspec-core#2736 |
I'm sorry I didn't check properly initially, and it's good that we've researched this.
Do you think there is anything else actionable? Are you happy with the current default settings? PS I deeply respect your persistence and strong opinions. |
Yes, I'm sure
Should prefer
Absolutely. I can imagine many people wanting to avoid
Besides enforcing There are very few legitimate uses of
Note that even
Edit: Actually I just realized that automagical predicate matchers accept arguments. So Other builtin matchersI didn't check if there's a cop for this, but I see a lot of cases where builtin matchers are not used.
Arrays are a typical example:
What's worse is that then the elements are tested one after the other:
When that fails, we have the whole array we can compare. Yes, I put the trailing comma out of spite, that's another default I disagree with 😆
Thank, I hope I don't come out too strong to others. |
I'm working on a pull request (not pushed yet) to address the issues of the
It turns out that the default # also good - It checks "true" strictly.
expect(foo.something?).to be(true) This goes against https://rspec.rubystyle.guide/#predicate-matchers that states "strict" usages as a bad example:
To add even more controversy, quoting Effective Testing with RSpec again:
So from my understanding, "strict" is worse than To be fair, I guess the justified predicate-look-alike methods like
To be continued... |
Right, I agree with that quote, but only in the context of applications. For gems I believe that the behavior we care about is the exact behavior so specs should be quite strict. |
Just got more autocorrections for: expect(something).to be_a(SomeClass)
# corrected to
expect(something.is_a?(SomeClass)).to be(true) That's also equally bad, for the same reasons as the rest. |
Even though I agree that gems should be more specific in regards to their public API, they may choose to be more relaxed when testing private APIs. |
I'm not sure I understand. Do you think |
They may. I'll disagree with the fact that they are testing private APIs 😆. |
Sorry, I haven't looked into the cop, or its settings. I don't know what the All I know is that To summarize, my position is that, by default (or at least in RuboCop):
I have no informed opinion as to what the settings should be named / how the cops should be split. Just that the defaults are horribly wrong for |
To avoid any ambiguity, I didn't mean testing private methods. |
I actually disagree with the quote:
I think it's best to specify things as tightly as possible. It's too easy for subtle bugs to sneak into code when we aren't precise in what we expect. That being said, I do find the My thinking on it at this point is basically that we should be explicit if it is the predicate method under test. This ensures the method returns # good
describe '#foo?' do
it 'returns false' do
expect(thing.foo?).to be(false)
end
it 'returns true when fooed' do
thing.foo!
expect(thing.foo?).to be(true)
end
end
describe '#foo!' do
it 'makes it foo' do
thing.foo!
expect(thing).to be_foo
end
end
# bad
describe '#foo?' do
it 'returns false' do
expect(thing).not_to be_foo
end
it 'returns true when fooed' do
thing.foo!
expect(thing).to be_foo
end
end
describe '#foo!' do
it 'makes it foo' do
thing.foo!
expect(thing.foo?).to be(true)
end
end That's a little hard to lint, though, so we generally go with the explicit approach. |
I was convinced that
That's also the behavior I believe it should have. I'll open an issue. If they agree it's a bug, we'll fix it, otherwise I'll propose a setting for tight predicate matchers that we should turn on. I still much prefer the error messages with these matchers |
I think it would break a lot of tests if RSpec were to change it. Maybe they could add a configuration for |
I'm very curious about that, but if so it might hide bugs.
I proposed just that in your issue |
- expect(ary.size).to be(3)
+ expect(ary).to have(3).items IIRC, - expect(ary.size).to be(3)
+ expect(ary).to have_attributes(size: 3) but for me personally it looks like a bad replacement. |
- expect(ary.size).to be(3)
- expect(ary[0]).to some_matcher
- expect(ary[2]).to some_other_matcher
+ expect(ary).to match_array [
+ some_matcher,
+ anything,
+ some_other_matcher,
+ ] # Note: size checked implicitly This appeals to me and I'm using this a lot with expect([1, 2, 3]).to match_array([be.>(2), be_even, eq(1)]) In this case, expect(ary).to eq([
some_matcher,
anything,
some_other_matcher,
]) # Note: size checked implicitly |
Oh, indeed, my bad. Your example is what I should have written. |
Let me start with splitting the spec file into strict and non-strict, it will pave the way to splitting the cop. |
I've discovered expect([-1,2,3].all? { |x| x.positive? }).to be(true) The difference is in the failure message.
expect([-1,2,3]).to be_all { |x| x.positive? }
expect([-1,2,3]).to all be(&:positive?) it's surprisingly a success. I'll file an idea ticket for a cop and a ticket for RSpec to emit a warning/fail if a block is passed to
The latter seems to be the most informative. |
Good catch on the block 👍
Definitely the best 🎉 |
I disagree about I write Another example is when the data or code is not entirely under my control. If I'm receiving JSON from an API if it's Similar for Perhaps this is a difference between writing application code (be lax in what you receive) and gem code (be strict in what you send). One thing is clear: be_truthy/be_falsey should be its own cop separate from PredicateMatcher. Over-specifying makes it more likely the cop will be disabled. |
Agreed, usually it doesn't matter. But it might, e.g.
Possibly, but typically you shouldn't spec what you receive. Note that I wrote "at least for
I'm curious as to why you'd want to have specs for that.
👍 |
@pirj have you been able to make progress on this front? |
@marcandre Not much code-wise. Started splitting up the specs to allow to split cops. https://github.com/rubocop-hq/rubocop-rspec/compare/predicate-matcher-streamline-specs Adding descriptive tickets with thoughts that born in the discussion above. |
I wouldn't want to impose on your priorities; it's only an irritant everytime I see specs like this, so I was just curious. Definitely way higher priority than this though! 😆 |
Indeed! 😆 |
I gave it some thought and came to this conclusion: I did find some value in examining Of 5579 examples there's roughly 150 variations of be_falsey, be_truthy, be nil, be false, be true. The concerns I had about API wobble don't seem to be evident. This is not an overwhelming amount to review, but the opportunity cost is high. I would disable the cop in this existing project, but use it in a fresh project. What I'm finding using Rubocop is when the default settings seem to be over-specified, rather than switching them off, sometimes its teaching me a lesson. However, the rationale is rarely documented and I have to puzzle it out for myself. I would ask that the rationale for the default styles be documented, it would speed up the process.
👍 |
An additional caveat for RSpec/PredicateMatcher. I found it makes refactoring harder, and its harder for my junior to understand the specs. My junior programmer followed Naming/PredicateName to change I like the concept of predicate matchers, but I'm hitting a lot of caveats. |
No doubt there's less magic with the explicit style, and
Cop class docs are later published as cop docs. |
Interesting anecdote. It's clear this involves a bit of cognitive load. What I like about it is there was very little cost in learning it, in that the spec failed the minute the predicate was renamed. Your junior learned something and will now be able to write specs this way. If ever the renaming was part of a bigger commit (and thus harder to understand), your junior hopefullly learned to break his changes in smaller refact commits 😆 |
I agree with this sentiment. I tend to use RSpec’s built-in matchers, but stay away from the more “magic” dynamic predicate matchers. And the reduced “greppability” is a big factor here. Probably I miss out on some better error messages, but in my opinion (at least in the code base I most commonly work on) it’s a tradeoff I‘m willing to make. |
@pirj any progress? I had a momentary lapse and wrote |
No progress here. Your case is strange. RSpec.describe do
it do
expect(some_array).to be_empty
expect(some_array.empty?).to be_truthy
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RSpec/PredicateMatcher: Prefer using be_empty matcher over empty?
expect(some_array.empty?).to be(true)
end
end Can you check? It doesn't seems to behave as with the default setting for |
No need to apologize, really :D |
Getting back to this 2 years later.
Right, this has its roots in Testing
Ouch, nearly 3x more offences. My plan is to:
With that change, there won't be any direct replacement for the current default. Do you agree with this course of action, @bquorning, @Darhazer ? |
We recently upgraded to the new defaults and have like ~1800 I think we're going to have to come up with a migration plan. Or maybe the author will find a time in the middle of the night to merge it. |
RSpec/PredicateMatcher
tells me to prefer the first form to the second one:This is gravely mistaken. It's longer, it's imo less clear, but much more importantly a failure (like in this example) gives uninformative info:
Notice how the second failure is much more informative, showing the unexpected error and all. That is the reason to always use precise matchers.
I don't mind if we keep the functionality for the (imo always bad) style, I don't care if we propose
be_nil
oreq(nil)
, but the default should change.Let's minimize the reasons to curse at RuboCop.
The text was updated successfully, but these errors were encountered: