-
-
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
Cop Idea: NoLet #862
Comments
This style exists and has its proponents, see discussion in the style guide. There's also a word of precaution in RSpec docs:
|
Thanks for the links. I'm not necessarily advocating for changing the style guide, though, just adding rules to support this style of writing tests. One thing I see differently from the person in the style guide thread is that I also prefer to avoid instance variables. To me they have their own caveats that make them troublesome. For example, they are un-lintable. Rubocop cannot tell us when they are unused, and when we misspell one, we might get a confusing error or no error at all. I've had tests where the instance variables were misspelled for one reason or another and the test doesn't fail: expect(@undefined_var1).to eq(@undefined_var2) Also, there is a similar tendency to let(:doc) { create(:doc) }
# or `before { @doc = create(:doc) }`
it 'does a thing when doc is draft' do
doc.update!(status: 'draft')
...
end A method is more flexible. It allows us to have a default and also pass parameters to customize it. it 'does a thing when doc is draft' do
doc = create_doc(status: 'draft')
...
end |
Indeed. Would you still be interested in advocating helper methods in the style guide? There's a related discussion there. |
And, of course, a pull request for |
Fixes rubocop#862
Fixes rubocop#862
Fixes rubocop#862
What do you think of having a cop for a maximum number of |
@Darhazer I'm open to that. I've noticed that rules seem to avoid using |
I support the idea. We can even enable the cop by default with |
@pirj |
Good question. Also keeping in mind included contexts that might contain lets as well, it's an unreachable goal to properly count them all. |
Anyway, since it's Ruby, it sounds near impossible to count all Would you like to tackle this? I'll be happy to help you along the way. |
My bad, it slipped my mind that pull request existed 🤕 |
@pirj sorry, I haven't had a chance to follow up on this. I think maybe it makes sense to count |
I think it won't be fair, e.g.: RSpec.describe A do
subject { described_class.new(x) }
context 'with a' do
let(:x) { :a }
it { ... }
end
context 'with b' do
let(:x) { :b }
it { ... }
end
context 'with c' do
let(:x) { :c }
it { ... }
end
end would count 3, but there's actually just one in each given scope. |
Hmm, yeah, I guess that's true. I can try and set it up per-scope. I'll try to work on it this afternoon. |
Fixes rubocop#862
@mockdeep said:
Absolutely. That's one thing that
Since
Both helpers or
The equivalent of a parameter with a default is to refer to another def help_me(options = {})
# ...
end
# vs
let(:options) { {} }
let(:help_me) { # call options... } Moreover, it's much more cumbersome to pass an argument down two levels of
|
In short, it is definitely possible to shoot yourself in the foot with I have no objection that the cop be implemented, but I vigorously object to a default setting (of any value). Let's minimize the reasons to curse at RuboCop. |
@marcandre a lot of your arguments are addressed already here and in the linked discussions.
There are several differences. For one, a method can accept arguments to customize the result produced. For another, calling a method is more explicit and doesn't magically instantiate a record the moment it is referenced. Sometimes I need a record to be present for one of the tests, but I don't need to test anything against the record itself, or I don't need the record until the code is exercised, which leads to awkward tests: it 'verifies users' do
user # this feels really awkward
UserVerifier.call
expect(user.status).to be_verified
end vs: it 'verifies users' do
user = create_user # this communicates more clearly to me
UserVerifier.call
expect(user.status).to be_verified
end
The difference is that we can avoid extra unnecessary database queries by creating the record we need in the first place instead of updating them after the fact. Using a method allows for a more flexible interface: user.update!(status: 'active')
# vs
user = create_user(status: 'active')
Not quite. Again, having a method allows you to customize the result on a per-test basis, not just being limited to a
You can define methods in the same way, overriding them in contexts, though I'd argue overriding
Methods work in the same way. They can be accessed from shared examples, too. In general, methods provide both greater flexibility and explicitness, which tends to make tests way easier to extend and maintain. |
I side with @mockdeep in the I respect and understand |
user.update!(status: 'active')
# vs
user = create_user(status: 'active') I agree that the first example is not the way to go. The context 'when the user is active' do
let(:user_options) { {status: 'active'} }
# ...
end That solution is, imo, superior to
The only functional difference is if you want to call that functionality twice. Otherwise you can always wrap your test in a context.
|
I'll repeat: I have no problem with the cop, I have a problem with enabling it as a default for RuboCop. I can't imagine the look of a PR that removes the |
Maybe I'm misunderstanding... Are you suggesting that instead of: let(:source) { 'code = {some: :ruby}' }
let(:ruby_version) { '2.4.0' }
let(:processed_source) { #... using source and ruby_version } This be written as: def source; 'code = {some: :ruby}'; end
def ruby_version; '2.4.0' end
def processed_source(source: source, ruby_version: ruby_version)
# ...
end So that one could write |
@marcandre https://github.com/rubocop-hq/rubocop-rspec/blob/master/spec/rubocop/rspec/hook_spec.rb is a good no-let example. I believe there can be more. I'm up for an experiment to change some spec that makes use of |
No worries, I doubt this is ever going to happen. |
That example was meant to suggest some variability between tests. A more thorough example would be something like: it 'does a when user is "active"' do
user = create_user(status: 'active')
end
it 'does b when user is "pending"' do
user = create_user(status: 'pending')
end
it 'does c when user is "blocked"' do
user = create_user(status: 'blocked')
end
It's easy to boil it down to a single difference, but it applies in a variety of situations, like the one above where it makes it easier to simplifiy the test structure. As you mention, it allows you to create multiple records in the same test: active_user = create_user(status: 'active')
pending_user = create_user(status: 'pending') It helps make each test more declarative and explicit about its dependencies. And aside from those, it can also make for a nice way to extract other repeat logic between tests, as well as potentially moving repeated chunks further up for re-use elsewhere.
The argument is only partly about explicit passing of arguments, as mentioned above. It's also about flexibility and simpler test structure, among other things.
That's one alternative. It depends on what sort of flexibility you need. If you only need them as constants in your tests, you can actually assign them as locals in you context blocks: context 'my feature' do
# don't declare them as CONSTANTS, though, or they will bleed up to the global scope
source = 'code = {some: :ruby}'
ruby_version = '2.4.0'
it { expect(ruby_version).to eq('2.4.0') }
end A method won't be able to access those directly, though. So they'd need to be passed in, or hard coded as defaults. If you're dealing with shared examples, you can pass parameters in: RSpec.shared_examples 'MyThing' do |yabba, dabba|
it { expect(yabba).to eq(dabba * 2) }
end
it_behaves_like 'MyThing', 4, 2 Or with keywords: RSpec.shared_examples 'MyThing' do |yabba:, dabba:|
it { expect(yabba).to eq(dabba * 2) }
end
it_behaves_like 'MyThing', yabba: 4, dabba: 2 This latter has the benefit of throwing an error early with a nice Bottom line, methods are probably a good default.
I don't think anybody has been seriously arguing in favor of this. As much as I think this is a better approach, I just want tooling to help guide the codebases I work on. We talked about shifting the naming to something that allows limiting the number of |
Fixes rubocop#862
Fixes rubocop#862
Fixes rubocop#862
@marcandre a question to you since you've been vocal in this discussion on the side of not enabling this cop by default. I understand that this is a |
@bquorning @Darhazer Why disable any cop by default? It will go under the radar. We offer more cops to satisfy any style. Some of the cops are configurable, and some have to be turned off in the config to meet the style. Out of currently disabled by default cops I see:
It's not a big deal to turn off one cop on version update. Especially when it's just once a year. Especially compared to the main RuboCop where new cops appear several times a month. There's yet another thing, not widely used for sure, |
Me, vocal? Never 😅
I'm not sure... What is the current maximum number of local variables and the maximum of To convince me, maybe someone could refactor this spec file and show me how it improves it? |
That sounds like a challenge, and I accept it :D |
I can remember those said limitations https://thoughtbot.com/blog/sandi-metz-rules-for-developers, but nothing related to local variables and methods. |
Fixes rubocop#862
Fixes rubocop#862
Fixes rubocop#862
Fixes rubocop#862
Fixes rubocop#862
Fixes rubocop#862
Related to rubocop/rubocop-rspec#862 This adds a `spaces` helper method and simplifies the `other_cops` `let` blocks. There's still more that could be done to make these tests a little easier to maintain, but thought this was a decent first step. It's also probably easier to discuss in discrete steps.
It would be nice to have a rule that disallows the use of
let
. I've been moving in the direction of avoiding them in my tests. Instead I'll err on the side of a little more duplication in my tests. In cases where there is a lot of setup, I'll instead define methods or factories to encapsulate the logic. More in this thoughtbot article.The text was updated successfully, but these errors were encountered: