-
-
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
Cop suggestion: Detect unnecessary usage of let!
#94
Comments
let!
slet!
👍 |
@andyw8 this is implemented in master |
Referring to the example let!(:my_widget) { create(:widget) } Care to weigh in? I'll be able to provide context later potentially but I'll let you respond since you originally proposed this cop |
If this is a problem, why hasn't it be proposed for deprecation/removal in rspec itself? |
@jcoyne we aren't flagging all usage of |
I guess I don't see what the harm/benefit of that is. If anything the name is giving some context to the reader of the spec. |
Fixes #296 This is getting too heavy handed and imposing too much maintenence. Here are a few examples: rubocop/rubocop-rspec#149 rubocop/rubocop-rspec#94 rubocop/rubocop-rspec#23
Before I defend the cop further I'll note that these cops are supposed to be opinionated. This is why many are configurable. You're always free to disable particular cases, disable cops for a directory, or disable them entirely. Consider the following snippet of code def write_user
user = database.persist(user_info)
do_some_work
do_some_other_work
true
end I think this is similar to the argument you are making about an unreferenced Likewise, consider this spec: let!(:user) { database.write_user(user_info) }
before do
do_some_work
do_some_other_work
end
it "asserts something" do
expect(something_that_depends_on_db_state).to be(true)
end I think it is odd to read over this spec, see the assignment to |
Closes rubocop#94
We should at least disable this cop by default. Quite the opposite: |
@dpisarewski can you give me an example of "disabling" a |
@backus For example: describe Product do
subject { product }
let!(:product) { create(:product) }
it { is_expected.to be }
context 'when product does not exist' do
let!(:product) { nil }
it { is_expected.to be_nil }
end
end |
Closes rubocop#94
Another reason I think this is a bad rule: If you create records and perform other setup in a let!(:nonmatching_user_in_same_organization) { FactoryBot.create(:user, organization: user.organization) } In a |
@dpisarewski I think If you need to "undo" describe Product do
subject { product }
context 'when prooduct exists' do
let!(:product) { create(:product) }
it { is_expected.to be }
end
context 'when product does not exist' do
it { is_expected.to be_nil }
end
end @RobinDaugherty For descriptive name - you may use just variable name with describe Foo do
context 'when .... ' do
before do
_nonmatching_user_in_same_organization = FactoryBot.create(:user, organization: user.organization)
end
end
end |
What benefit is there to placing these in a The alternative of using a To return to the original description of this PR:
Placing setup in an The point of this comment thread is that this rule is not well though-out and wasn't discussed prior to implementation. The majority of reactions to this issue are downvotes. There was no discussion as to the relative benefits of the rule before it was merged in #162 and enabled by default. Multiple people have asked for that default to be changed. It seems like a bad default that will lead to worse practices, not better. |
You make strong arguments in support of this statement, but it's not black and white. The Arrange/Act/Assert style of unit testing advocates putting the setup in each unit test. Sharing setup and context between tests may be a code smell. Perhaps the system under test needs a redesign to simplify its setup. Or, the setup could be extracted such that its just a method call in each test. |
If you need to refer to a value across example groups, a Imagine you had this code: it 'does something' do
current_user = User.create!(...)
expect(Users.count).to be(1)
end RuboCop (and ruby, actually) would flag current_user as an unused variable. Why is the lvar created if it's not going to be used? Remove the assignment and add a comment if you need to explain why it is there (or you can also underscore the variable). What this cop is advocating is similar: If all you want to do is provide some sort of side effect for setup, that's what If you have a test where you need to do As an aside, I think the many uses of
I think it's slightly easier to have errant
I'm going to have to disagree with this blanket rule. If the setup is only applicable to a single example, I usually prefer inlining it. It's actually more lintable and, imo, it's easier to understand in one glance since it's all in one place. Your point about adding extra examples requiring an additional step is valid, but it's a tradeoff I'm frequently happy to take. I find I avoid more convoluted RSpec setup this way (I often have to modify I don't have that strong of a feeling about this cop because I rarely use
I don't think these arguments are accurate or constructive. I think you are assigning blame where none is due:
And, as with all cops, the user is free to disable it for themselves--RuboCop is supposed to be configurable. |
Look, even the style guide disagrees with this rule.
I recommend you adopt better practices for deciding on changes to default rules, and these should probably include following the project's style guide and deliberating on a new rule before adopting it in the default ruleset. |
I agree this should be made consistent. I think this advice should be removed from the style guide. As an aside, I don't care for having a style guide just for the difficulty of keeping things like these in sync. Personally I think we should just generate something directly from the config, but that's another problem for another day.
I think the thought, going forward, for rubocop (and I assume rubocop-* projects) is to make all new cops opt-in until a major version is released when they become default but I can't remember all the details. Historically introducing a disabled cop didn't make much sense because no one would ever find it so people have just added and enabled all new cops and users can disable as they like. Defaults are changed pretty rarely outside of newly added cops. |
@chulkilee Why would changing a value be a hack? That's one of the basic things we do in imperative programming languages, ruby in particular. I tried to keep the example as slim as possible, please don't focus on irrelevant details. The point is you often have a shared setup in which you make little changes for a particular use case. If you don't share setup, then you have to deal with a huge code duplication. Here is a more complex example which I hope can make my point more understandable: describe Product do
subject { product }
let!(:product) { create(:product) }
let!(:product_category) { create(:product_category, 'some_category') }
let!(:product_images) do
[
create(:product_image, 'some_image', product: product)
]
end
let!(:product_attributes) do
[
create(:product_attribute, key: :price, 1, product: product)
create(:product_attribute, key: :description, product: product)
]
end
let!(:product_variants) do
[
create(:product_variant, product: product)
]
end
let(:found_description) { product.attributes.find_by(key: :description) }
let(:found_price) { product.attributes.find_by(key: :price) }
it 'has a description' do
expect(found_description).to be
end
it 'has a price' do
expect(found_price).to be
end
context '#images' do
subject { product.images }
it { is_expected.to have(1).item }
end
context '#attributes' do
subject { product.attributes }
it { is_expected.to have_at_least(2).items }
end
context '#variants' do
subject { product.variants }
it { is_expected.to have(1).item }
end
context 'dummy' do
let(:placeholder_image) { create(:product_image, 'placeholer', product: product) }
let!(:product_images) { [placeholder_image] }
it 'has only 1 image' do
expect(product.images).to have(1).item
end
it 'has a placeholder image' do
expect(product.images.first).to eq(placeholder_image)
end
end
context 'generic' do
let!(:product_attributes) do
[
create(:product_attribute, key: :description, product: product)
]
end
it 'has no price' do
expect(found_price).to be_nil
end
end
context 'multivariant' do
let!(:product_variants) do
[
create(:product_variant, product: product)
create(:product_variant, product: product)
]
end
it 'has many variants' do
expect(product.variants).to have_at_least(2).items
end
end
end You can extract the setup of this data into factories, which would make it more complex and it's beyond the scope of this discussion. |
In the opposite, this pattern is about separation of Setup, Action and Assertion phases, not about putting everything together. The code on the page is just a trivial example without using any testing frameworks(the example uses blank lines to separate the phases). Every testing framework defines a DSL/API to separate those, eg. |
@dgollahon In Soviet Russia specifications refer to implementations. 😀
Why not remove the cop? I think, we should invert the cop, so From my experience In general, I don't get why it hurts you to name a piece of code when the name is not used programmatically. In my opinion it greatly improves code readability and is preferred over comments in ruby community. |
Just for your information, this cop was added two years ago, while the There was a bit of discussion in the style guide PR (rubocop/rspec-style-guide#55 (comment)) and I believe the intention is to change the style guide. See e.g. rubocop/rspec-style-guide#56 which references the RSpec maintainer’s comment on a Reddit AMA (https://www.reddit.com/r/ruby/comments/73nrhk/ama_the_authors_of_effective_testing_with_rspec_3/dnsyanp/):
|
Don't get me wrong - I'm not against of overriding values with What I'm calling "a hack" is returning Here is an example of doing that: describe Product do
let!(:product_image) { create(:product_image) } # <- is it really needed for all examples?
describe '#images' do
it 'returns an image' do
# ...
end
context 'having no image' do
let!(:product_image) { nil } # <-- I'd call it as a "hack"
it 'does not return an image' do
# ...
end
end
end
end This can be refactored like this - without overriding describe Product do
describe '#images' do
context 'having an image' do
# now let! makes sesnses with the context
let!(:product_image) { create(:product_image) }
it 'returns an image' do
# ...
end
end
context 'having no image' do
it 'does not return an image' do
# ...
end
end
end
end There are many ways to group examples - for example if you have very large test cases for a setup, then it might be bettero to have separate rspec files, instead of placing all tests under one file - e.g. Also please note that youmay just use plain ruby helper module or factory bot trait to set up, instead of applying lots of |
@chulkilee It sounds like a big hack 😱
@dgollahon 15 downvotes to be exact 😉 |
so we're still discussing this, a few years after it first came up. I am on the fence of yes these have been abused over time, but mostly by newer-to-the-profession engineers who are trying to figure it all out, and for rails apps for which every-guest is both a mystery and a first-class participant. In actuality, over the past dozen or so years doing this, I can safely say that at least 70% of the apps i've worked on have always-enabled objects that form a basic part of the domain. Who can imagine a webapp that doesn't require some kind of user object? So, we're forced to use mechanics like this to establish our orchestra before we begin the recital. Yes, we could use before blocks, but we're also encouraged to move towards smaller blocks of code that don't repeat-- something that using This cop doesn't have any true basis in best practice. I have not found any reasonable reference in the style guide. I know that original reasoning for creating the behavior was designed to set this up. So all I have left is to accept this is @andyw8's (or shopify's) personal preference, and not necessarily something that matches the entire community. Strong advocate to making this disabled-by-default moving forward. |
My initial post was here from four years before I joined Shopify. Anything I post here is in a personal capacity and does not reflect any Shopify preference. (also, RSpec is rarely used at Shopify anyway). |
Ah, thanks for clarifying! I didn't know :D :D |
Let me try to add an aspect to the discussion which wasn't mentioned yet: readability of tests. Setups like the one mentioned by @chulkilee (
|
This comment has been minimized.
This comment has been minimized.
This hits the nail on the head. I wonder if RSpec has encouraged a little too much DRYing which have hence led to less readable tests? |
I often come across code which uses
let!(...)
for setting up state, but then never makes any direct reference to those identifiers. This setup should be in abefore
block, or inlined into the example'sit
block:Bad
Good
or
The text was updated successfully, but these errors were encountered: