-
-
Notifications
You must be signed in to change notification settings - Fork 277
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
Add new RSpec/PreferredSharedExampleMethods
cop to enforce consistency when defining shared examples
#2065
Conversation
.rubocop.yml
Outdated
PreferredExamplesMethod: shared_examples | ||
PreferredContextMethod: shared_context | ||
PreferredIncludeExamplesMethod: include_examples | ||
PreferredIncludeContextMethod: include_context |
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 chose the most commonly used methods in the codebase:
shared_examples
was used 10 times andshared_examples_for
was used 4 times.include_examples
was used 42 times andit_behaves_like
was used twice.
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.
Just fyi, include_examples
and it_behaves_like
don't do the same thing (unlike shared_examples
and shared_examples_for
which are synonyms).
Most projects I worked on typically used both, include_examples
for example can't be used if you want to do multiple includes which override each other's setups:
RSpec.describe do
shared_examples 'foo' do |var|
subject { var }
it { is_expected.to eq(var) }
end
include_examples 'foo', 1 # fails because the second include sets subject to 2
include_examples 'foo', 2
end
so you need to use it_behaves_like
instead to wrap each include in a context.
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.
Thank you for this, I did not realize they behaved differently. So a few thoughts:
- The easiest solution is probably just to leave
PreferredIncludeExamplesMethod
unset (which I should probably do here, because I didn't realize the distinction), and allow users to set it if they exclusively want to use one or the other. - It is also possible to do what I did in the spec for this cop and define a top level context within the shared examples, in which case
include_examples
still works since the examples have their own contexts. The changedit_behaves_like
examples still work for the same reason, but it might be worthwhile to undo that change. - The
Language
hash doesn't distinguish between the two which is what this cop is based on. I'm not sure if it should, but that is outside the scope of this PR at the least. - Given that
include_examples
doesn't create a context, we should probably have a cop that looks for shared examples that setsubject
orlet
that then are called withinclude_examples
multiple times in the same context. Again outside of the scope of this PR, but would be good to prevent a footgun. - The other solution here would be to remove the
PreferredIncludeExamplesMethod
configuration, or even not consider includes at all. I don't think it's a huge risk to keep it given that it defaults to unset (ie. all methods are acceptable). - We could theoretically treat
include_examples
differently, but again because the different Includes are not distinguished in theLanguage
hash, the problem could still exist if it is aliased. - At the very least we should document this distinction.
/cc @bquorning @pirj
495830d
to
5205c8a
Compare
f695914
to
ed551f6
Compare
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.
@pirj, what do you think?
As @lovro-bikic mentions, The other - whether to include examples or include them into nested context, There are use cases for
Methods as well. Also, There may be another cop that would detect shared contexts that contain examples, and yet another, that will detect no examples in a shared example group. We may have an issue to track this idea already. I've grepped real-world-rspec, and usage stats are like (rough aliases: include_examples - 4450 aliases:
My proposal:
|
ed551f6
to
f315ea9
Compare
@pirj thank you for the feedback! I have updated this PR to only apply to defining shared examples.
I'll work on this next. |
RSpec/PreferredSharedExampleMethods
cop to enforce consistency when defining and including shared examplesRSpec/PreferredSharedExampleMethods
cop to enforce consistency when defining shared examples
a5552f5
to
cc777b3
Compare
BTW I just realized that |
I am sorry to have missed that. We seem to be in the spot where we can’t remember all the cops we already have 😅 |
cc777b3
to
eacb4d6
Compare
@pirj @bquorning anything else needed for this one? |
spec/rubocop/cop/rspec/preferred_shared_example_methods_spec.rb
Outdated
Show resolved
Hide resolved
spec/rubocop/cop/rspec/preferred_shared_example_methods_spec.rb
Outdated
Show resolved
Hide resolved
spec/rubocop/cop/rspec/preferred_shared_example_methods_spec.rb
Outdated
Show resolved
Hide resolved
I was always curious, and this question reveals each time when I think about directory-local .rubocop.yml which could come handy in a case when there’s an alias defined eg ‘shared_context’ is preferred in specs/, but shared_state in features/ How does it work with multi-threaded RuboCop, that nechanism that parallelizes execution? There should be separate ConfigLoaders (or whatever they are called). How does rubocop-rspec work in this case? We do have a global Language module that keeps a reference to the config? #racecondition Can’t this be a source of flaky subtle bugs? |
The question above is totally unrelated to the PR’s code. |
…ncy when defining shared examples
eacb4d6
to
f28054f
Compare
@pirj I've addressed your requests now. |
I tried this out and it should work how you expect. You can create .rubocop.yml files in subdirectories, and they do get loaded (and can inherit from the main one too).
There's a lot of layers to investigate here, but ultimately what happens in short is that each inspected file mobilizes a team of cops, during which a config object is created for that file. So each file gets the correct config (there's some caching involved as well) for the directory it's in.
Language has a class level attr_accessor Therefore, per-directory
I don't think so because even with parallelization, each file runs in a single thread. |
Description: Checks for consistent method selection for shared examples. | ||
Enabled: pending | ||
VersionAdded: "<<next>>" | ||
PreferredExamplesMethod: ~ |
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.
Discussion starter. Should we provide a setting here? Otherwise, the cop is a no-op, no?
Rarely people jump and configure new cops. Unless they’ve implemented them themselves 😅
There’s evidence that one style prevails, but I’m not sufficiently confident that we should.
Why do people use the shared_examples_for? (We did as the second commit says). Does it read better in some cases?
Why do we implement this cop in the first place? Do we want to get rid of shared_examples_for? Or do we want to enforce use of aliases where due? Or prevent such usage?
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 think it's useful to enforce consistency, there are plenty of other examples of this across RuboCop of cops that are just meant to enforce one style or another when there isn't an actual difference between them. For instance, Style/CollectionMethods
allows users to choose to enforce map
vs. collect
, etc.
I didn't specify actual defaults because I don't think there is one answer (but we could if we want...).
Why do we implement this cop in the first place?
In a codebase with many contributors, there is no particular reason (IMO) to use one alias over the other so it becomes inconsistent. I noticed this in the main rubocop repo and wanted to fix it as an InternalAffairs cop, and it was suggested to add a RuboCop::RSpec cop instead 😆 In the meantime, the inconsistency was fixed directly in rubocop/rubocop#14034.
@@ -775,6 +775,14 @@ RSpec/PredicateMatcher: | |||
StyleGuide: https://rspec.rubystyle.guide/#predicate-matchers | |||
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/PredicateMatcher | |||
|
|||
RSpec/PreferredSharedExampleMethods: |
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.
On a different note, I suddenly recalled we have RSpec/Dialect cop 🙈😭
I sincerely apologise if that other cop does what this one does with aliases, again, it slipped my mind as I’m less involved with the project lately.
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.
Oh wow! I didn't know about it either. The tests for it are pretty sparse and the documentation is a bit confusing as to how to configure, but it does seem to do what I was looking to do.
RSpec/Dialect:
Enabled: true
PreferredMethods:
shared_examples_for: shared_examples
With this in mind, we can probably close this PR.
Thank you for the in-depth explanation, @dvandersluis ! How does context switch happen, how do we load multiple cores? Do we have a pool of rubocop processes, each with its own set of class-level attributes? Or can a thread context switch happen before the cop finished the investigation, and some other cop would overwrite the cached language DSL config? |
I'm going to close this PR given |
Allows RSpec/SharedExamples to be configured with preferred methods for defining shared examples and context, so that consistency can be enforced.
Follows rubocop/rubocop#14033, and reimplementation of #2062.
Before submitting the PR make sure the following are checked:
master
(if not - rebase it).CHANGELOG.md
if the new code introduces user-observable changes.bundle exec rake
) passes (be sure to run this locally, since it may produce updated documentation that you will need to commit).If you have created a new cop:
config/default.yml
.Enabled: pending
inconfig/default.yml
.Enabled: true
in.rubocop.yml
.VersionAdded: "<<next>>"
indefault/config.yml
.If you have modified an existing cop's configuration options:
VersionChanged: "<<next>>"
inconfig/default.yml
.