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

Thin out lets in random spec #8447

Closed
wants to merge 3 commits into from
Closed

Thin out lets in random spec #8447

wants to merge 3 commits into from

Conversation

pirj
Copy link
Member

@pirj pirj commented Aug 3, 2020

Related to rubocop/rubocop-rspec#862

To convince me, maybe someone could refactor this spec file [to reduce the number of used memoized helpers] and show me how it improves it?


Before submitting the PR make sure the following are checked:

  • [no] Wrote good commit messages.
  • [-] Commit message starts with [Fix #issue-number] (if the related issue exists).
  • Feature branch is up-to-date with master (if not - rebase it).
  • [no] Squashed related commits together.
  • Added crushed tests.
  • [-] Added an entry to the Changelog if the new code introduces user-observable changes. See changelog entry format.
  • The PR relates to only one subject with a clear title and description in grammatically correct, complete sentences.
  • Run bundle exec rake default. It executes all tests and RuboCop for itself, and generates the documentation.

@pirj
Copy link
Member Author

pirj commented Aug 3, 2020

@marcandre refactoring is done, I'm ready for the harder part, part two, "show me how it improves it".

@pirj pirj marked this pull request as ready for review August 6, 2020 23:33
pirj added a commit to mockdeep/rubocop-rspec that referenced this pull request Aug 7, 2020
Switching the cop on by default to match RSpec recommendations from its
docs and the community RSpec Style Guide recommendations.

    `let` _can_ enhance readability when used sparingly (1,2, or
    maybe 3 declarations) in any given example group, but that can
    quickly degrade with overuse. YMMV.

https://github.com/rspec/rspec-core/blob/5d87c28bb828ba14b54a0d8cfddf044894009314/lib/rspec/core/memoized_helpers.rb#L257

https://reddit.com/r/ruby/comments/73nrhk/ama_the_authors_of_effective_testing_with_rspec_3/dnsyanp/

https://rspec.rubystyle.guide/#let-blocks

Example change to dramatically reduce the number of used memoized
helpers without resorting to methods rubocop/rubocop#8447

There is no limit to the number of memoized helpers being used in
practice:

10 lets https://github.com/forem/forem/blob/master/spec/support/api_analytics_shared_examples.rb#L2
20 lets https://github.com/chef/chef/blob/master/spec/unit/provider/systemd_unit_spec.rb#L23
30 lets https://github.com/chef/chef/blob/master/spec/unit/policy_builder/policyfile_spec.rb#L24
40 lets https://github.com/diaspora/diaspora/blob/develop/spec/integration/migration_service_spec.rb#L7
50 lets https://github.com/chef/chef/blob/master/spec/functional/resource/dsc_script_spec.rb#L71

Allowing `subject` by default, i.e. subjects don't count towards the
number of memoized helpers, since there is a dedicated
`RSpec/MultipleSubjects` cop. Combination with it provides infinite
possibilities to tweak the maximum allowed number of memoized helpers to
meet the project style.
pirj added a commit to mockdeep/rubocop-rspec that referenced this pull request Aug 7, 2020
Switching the cop on by default to match RSpec recommendations from its
docs and the community RSpec Style Guide recommendations.

    `let` _can_ enhance readability when used sparingly (1,2, or
    maybe 3 declarations) in any given example group, but that can
    quickly degrade with overuse. YMMV.

https://github.com/rspec/rspec-core/blob/5d87c28bb828ba14b54a0d8cfddf044894009314/lib/rspec/core/memoized_helpers.rb#L257

https://reddit.com/r/ruby/comments/73nrhk/ama_the_authors_of_effective_testing_with_rspec_3/dnsyanp/

https://rspec.rubystyle.guide/#let-blocks

Example change to dramatically reduce the number of used memoized
helpers without resorting to methods rubocop/rubocop#8447

There is no limit to the number of memoized helpers being used in
practice:

10 lets https://github.com/forem/forem/blob/master/spec/support/api_analytics_shared_examples.rb#L2
20 lets https://github.com/chef/chef/blob/master/spec/unit/provider/systemd_unit_spec.rb#L23
30 lets https://github.com/chef/chef/blob/master/spec/unit/policy_builder/policyfile_spec.rb#L24
40 lets https://github.com/diaspora/diaspora/blob/develop/spec/integration/migration_service_spec.rb#L7
50 lets https://github.com/chef/chef/blob/master/spec/functional/resource/dsc_script_spec.rb#L71

Allowing `subject` by default, i.e. subjects don't count towards the
number of memoized helpers, since there is a dedicated
`RSpec/MultipleSubjects` cop. Combination with it provides infinite
possibilities to tweak the maximum allowed number of memoized helpers to
meet the project style.
pirj added a commit to mockdeep/rubocop-rspec that referenced this pull request Aug 7, 2020
Switching the cop on by default to match RSpec recommendations from its
docs and the community RSpec Style Guide recommendations.

    `let` _can_ enhance readability when used sparingly (1,2, or
    maybe 3 declarations) in any given example group, but that can
    quickly degrade with overuse. YMMV.

https://github.com/rspec/rspec-core/blob/5d87c28bb828ba14b54a0d8cfddf044894009314/lib/rspec/core/memoized_helpers.rb#L257

https://reddit.com/r/ruby/comments/73nrhk/ama_the_authors_of_effective_testing_with_rspec_3/dnsyanp/

https://rspec.rubystyle.guide/#let-blocks

Example change to dramatically reduce the number of used memoized
helpers without resorting to methods rubocop/rubocop#8447

There is no limit to the number of memoized helpers being used in
practice:

10 lets https://github.com/forem/forem/blob/master/spec/support/api_analytics_shared_examples.rb#L2
20 lets https://github.com/chef/chef/blob/master/spec/unit/provider/systemd_unit_spec.rb#L23
30 lets https://github.com/chef/chef/blob/master/spec/unit/policy_builder/policyfile_spec.rb#L24
40 lets https://github.com/diaspora/diaspora/blob/develop/spec/integration/migration_service_spec.rb#L7
50 lets https://github.com/chef/chef/blob/master/spec/functional/resource/dsc_script_spec.rb#L71

Allowing `subject` by default, i.e. subjects don't count towards the
number of memoized helpers, since there is a dedicated
`RSpec/MultipleSubjects` cop. Combination with it provides infinite
possibilities to tweak the maximum allowed number of memoized helpers to
meet the project style.
pirj added a commit to mockdeep/rubocop-rspec that referenced this pull request Aug 8, 2020
Switching the cop on by default to match RSpec recommendations from its
docs and the community RSpec Style Guide recommendations.

    `let` _can_ enhance readability when used sparingly (1,2, or
    maybe 3 declarations) in any given example group, but that can
    quickly degrade with overuse. YMMV.

https://github.com/rspec/rspec-core/blob/5d87c28bb828ba14b54a0d8cfddf044894009314/lib/rspec/core/memoized_helpers.rb#L257

https://reddit.com/r/ruby/comments/73nrhk/ama_the_authors_of_effective_testing_with_rspec_3/dnsyanp/

https://rspec.rubystyle.guide/#let-blocks

Example change to dramatically reduce the number of used memoized
helpers without resorting to methods rubocop/rubocop#8447

There is no limit to the number of memoized helpers being used in
practice:

10 lets https://github.com/forem/forem/blob/master/spec/support/api_analytics_shared_examples.rb#L2
20 lets https://github.com/chef/chef/blob/master/spec/unit/provider/systemd_unit_spec.rb#L23
30 lets https://github.com/chef/chef/blob/master/spec/unit/policy_builder/policyfile_spec.rb#L24
40 lets https://github.com/diaspora/diaspora/blob/develop/spec/integration/migration_service_spec.rb#L7
50 lets https://github.com/chef/chef/blob/master/spec/functional/resource/dsc_script_spec.rb#L71

Allowing `subject` by default, i.e. subjects don't count towards the
number of memoized helpers, since there is a dedicated
`RSpec/MultipleSubjects` cop. Combination with it provides infinite
possibilities to tweak the maximum allowed number of memoized helpers to
meet the project style.
let(:body) { "puts '#{spaces}'" }
let(:source) { "#{body} if condition" }
context 'modifier `if` that does not fit on one line' do
let(:body) { "puts ' '" }
Copy link
Contributor

Choose a reason for hiding this comment

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

Another possibility that occurred to me is to have a spaces helper method that accepts a count argument:

def spaces(count)
  ' ' * count
end

let(:body) { "puts '#{spaces(59)}'" }

It seems like seeing the actual count in the code might be useful. I haven't looked at any of the other tests, but if we do this sort of thing in more places it might be a good global helper.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ha! I was trying to avoid defining methods to replace lets because of an argument that it's basically the same thing.

But this spaces method clearly shows how methods can be better, since they do accept parameters. I really like it.

It's possible to go even further with something like:

def body(spaces:)
  "puts '#{' ' * spaces}"
end

Copy link
Contributor

Choose a reason for hiding this comment

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

I was trying to figure out a good way to factor out body but wasn't able to come up with an alternative that I thought I could sell. Personally, I'd probably actually copy paste it into each test. I think some duplication can be advantageous across tests, since it makes it a lot easier to write varying test cases, and it makes each test more self-contained. That, to me, is easier to maintain and understand without scrolling up and down the file (and other files) to trace through all of the implicit dependencies.

I think our programmer's inclination to DRY up code actually works against us when it comes to tests. In tests we often want logic that changes in small ways across each test, and that makes it harder to deduplicate without making things more difficult to understand and maintain.

Copy link
Contributor

Choose a reason for hiding this comment

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

One sort of thing I like to think about is how hard it would be to rearrange the tests in a file. I often want to restructure the tests in a file to tell a more clear story. If the tests resist being moved around, then they're too coupled to various implicit dependencies. Thinking about rearranging the tests in this file gives me panic sweats.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thinking about rearranging the tests in this file gives me panic sweats.

Indeed. And it's by far not the worst example.

}
end
let(:other_cops) { { 'Layout/LineLength' => line_length_config } }
shared_context 'with LineLength settings' do |enabled: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

This almost feels like cheating. The let is still there, it's just imported from a shared context. One other possibility that occurs to me is to have a line_length_config method:

def line_length_config(overrides = {})
  {
    'Layout/LineLength' => {
      'Enabled' => true,
      'Max' => 80,
      'AllowURI' => true,
      'IgnoreCopDirectives' => true,
      'URISchemes' => %w[http https]
    }.merge(overrides)
  }
end

let(:other_cops)  { line_length_config }
let(:other_cops)  { line_length_config('Enabled' => false) }
let(:other_cops)  { line_length_config('AllowURI' => false) }

Looking at other other_cops, it's not clear to me how many of the above settings are actually necessary. is 'Enabled' implied? And aren't the other settings the default when it is enabled? Could it just be:

let (:other_cops) { { 'Layout/LineLength' => { 'AllowURI' => false } } }

Looks like the Max default changed recently to 120, so not sure if we need to account for that.

Copy link
Member Author

@pirj pirj Aug 9, 2020

Choose a reason for hiding this comment

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

It's cheating in a way, yes. But also it's just hidden one let instead of three that we had before.

Why I chose that instead of a method is that we have to specify let(:other_cops) { ... }, in every place, which is somewhat clumsy.

Got an idea though, what do you think of:

RSpec.describe RuboCop::Cop::Style::IfUnlessModifier, :config do
  default_line_length_config =
     {
        'Enabled' => true,
        'Max' => 80,
        'AllowURI' => true,
        'IgnoreCopDirectives' => true,
        'URISchemes' => %w[http https]
      }

  def self.with_line_length_config(**overrides)
    let(:other_cops) do
      { 'Layout/LineLength' => default_line_length_config.merge(overrides) }
    end
  end

  context 'when AllowURI is false' do
    with_line_length_config(allow_uri: false)
  
    expect_no_offenses(<<~RUBY)
...

I hope they don't burn me as a heretic.
PS changed default_line_length_config to be a ... variable (hope it doesn't vary much though).

Copy link
Contributor

Choose a reason for hiding this comment

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

I actually went the route of setting other_cops in each location that needed it. I thought that ended up being easier to understand with less indirection. In some cases I set it to {} in order to unset it, which is what we're actually doing somewhat implicitly in the existing tests.

@mockdeep
Copy link
Contributor

mockdeep commented Aug 8, 2020

I poked around with some of this in my own PR here.

@marcandre
Copy link
Contributor

@marcandre refactoring is done, I'm ready for the harder part, part two, "show me how it improves it".

@pirj Did you actually want me to ask? I can if you want me to, but I can tell you already what I like in the PR: some changes that have nothing to do with this (like removing a "context" that's not really useful). Also, the originality of the commit messages ;-)

Otherwise I remain unconvinced that this is an improvement, and that it is worth whoever is writing this spec to worry about. The whole exercise seems as futile to me as reducing the number of local variables in a code.

@pirj
Copy link
Member Author

pirj commented Aug 9, 2020

I'm pretty happy already now when you don't mention that the spec looks significantly worse this way.
The whole point was to show that it is possible to write a spec by not using much lets. For the sake of merging a cop that would limit those usages to a certain number (5 lets allowed by default).

Otherwise, I'm ready for all the criticism. If you feel that some part of this became harder to understand/change/..., I'll be glad to hear your point on this. While working on cops/style guide I try to be as neutral as possible and accept distinct coding styles. So - no arguing included, just going to learn a new point of view.

No pushing/rushing, it's completely up to you, I will not be offended if you ignore this altogether, and I completely understand it - we're about to hit 1.0, just a few things left, and it will be beneficial to everyone.

@pirj pirj closed this Aug 9, 2020
@pirj pirj deleted the thin-out-lets branch August 9, 2020 16:55
@marcandre
Copy link
Contributor

I commented on #8491, partly because your PR's diff is noisier and appear to mix different improvements that are not related to the refactoring and thus felt harder to comment on.

Still, the bulk of your PR seems to be replacing some let with a shared context:

# before
let(:ignore_cop_directives) { true }
let(:allow_uri) { true }
let(:line_length_config) do
  {
    'Enabled' => true,
    'Max' => 80,
    'AllowURI' => allow_uri,
    'IgnoreCopDirectives' => ignore_cop_directives,
    'URISchemes' => %w[http https]
  }
end
let(:other_cops) { { 'Layout/LineLength' => line_length_config } }

# after
shared_context 'with LineLength settings' do |enabled: true,
                                              allow_uri: true,
                                              ignore_cop_directives: true|
  let(:other_cops) do
  {
    'Layout/LineLength' => {
      'Enabled' => enabled,
      'Max' => 80,
      'AllowURI' => allow_uri,
      'IgnoreCopDirectives' => ignore_cop_directives,
      'URISchemes' => %w[http https]
    }
  }
end

This introduces changes like:

# before
let(:allow_uri) { false }
# after
include_context 'with LineLength settings', allow_uri: false

I feel that using shared_context remains more technically and less known. The result is also more verbose. I like that this is a case where it's unlikely to make sense to want to change one setting of LineLength and only in a sub-context want to change another one, so your shared context makes clear that one can only specify these settings all at once. As for the verbosity, it makes things more explicit, I don't have a strong opinion on if it's better or worse overall, probably more like changing four quarters to a buck. In my mind, you are actually removing one let, the line_length_config; others are just shifted to keyword arguments to your shared context. It just so happens that the metric doesn't count those....

If I was reviewing some new Cop PR and got the current version with the lets, I wouldn't think twice about that and would not have an issue with this. If I got your shared_context version, I would be thinking "wow, looks like this submitter finds it so easy to write a shared context that it was worth it here, even given the additional verbosity", and I would also have no issue with that.

In short, I would have no trouble accepting either PRs, and I consider myself a tough reviewer. If I received a PR to switch from one to the other, I'd be wondering why, oh why, is the submitter focusing on that! If doesn't change much at all, just shifting how to specify allow_uri & al. I don't feel there's one way that is clearly superior to the other.

If one has time to spend on it, there are other things that would benefit more this spec. I notice there are many instances of expect(some_very_similar_string.length).to eq 79 for example. There's the strange "/#{'a' * 37}/" and probably a few other imperfections.

But ultimately, the biggest issue by far with this spec reveals a deep problem with the Cop itself: it knows way too much about the Layout/LineLength Cop. It should not be the job of IfUnless to know the existence of an Layout/LineLength/AllowURI setting! #8290 is but one solution to this. This is the kind of problems worth focusing on, in my mind.

@pirj
Copy link
Member Author

pirj commented Aug 9, 2020

Thanks! Your time is much appreciated.

@marcandre
Copy link
Contributor

My pleasure.

Maybe you did already, but did you say how much of an improvement you think the shared_context is?

pirj added a commit to rubocop/rubocop-capybara that referenced this pull request Dec 29, 2022
Switching the cop on by default to match RSpec recommendations from its
docs and the community RSpec Style Guide recommendations.

    `let` _can_ enhance readability when used sparingly (1,2, or
    maybe 3 declarations) in any given example group, but that can
    quickly degrade with overuse. YMMV.

https://github.com/rspec/rspec-core/blob/5d87c28bb828ba14b54a0d8cfddf044894009314/lib/rspec/core/memoized_helpers.rb#L257

https://reddit.com/r/ruby/comments/73nrhk/ama_the_authors_of_effective_testing_with_rspec_3/dnsyanp/

https://rspec.rubystyle.guide/#let-blocks

Example change to dramatically reduce the number of used memoized
helpers without resorting to methods rubocop/rubocop#8447

There is no limit to the number of memoized helpers being used in
practice:

10 lets https://github.com/forem/forem/blob/master/spec/support/api_analytics_shared_examples.rb#L2
20 lets https://github.com/chef/chef/blob/master/spec/unit/provider/systemd_unit_spec.rb#L23
30 lets https://github.com/chef/chef/blob/master/spec/unit/policy_builder/policyfile_spec.rb#L24
40 lets https://github.com/diaspora/diaspora/blob/develop/spec/integration/migration_service_spec.rb#L7
50 lets https://github.com/chef/chef/blob/master/spec/functional/resource/dsc_script_spec.rb#L71

Allowing `subject` by default, i.e. subjects don't count towards the
number of memoized helpers, since there is a dedicated
`RSpec/MultipleSubjects` cop. Combination with it provides infinite
possibilities to tweak the maximum allowed number of memoized helpers to
meet the project style.
ydah pushed a commit to rubocop/rubocop-factory_bot that referenced this pull request Apr 13, 2023
Switching the cop on by default to match RSpec recommendations from its
docs and the community RSpec Style Guide recommendations.

    `let` _can_ enhance readability when used sparingly (1,2, or
    maybe 3 declarations) in any given example group, but that can
    quickly degrade with overuse. YMMV.

https://github.com/rspec/rspec-core/blob/5d87c28bb828ba14b54a0d8cfddf044894009314/lib/rspec/core/memoized_helpers.rb#L257

https://reddit.com/r/ruby/comments/73nrhk/ama_the_authors_of_effective_testing_with_rspec_3/dnsyanp/

https://rspec.rubystyle.guide/#let-blocks

Example change to dramatically reduce the number of used memoized
helpers without resorting to methods rubocop/rubocop#8447

There is no limit to the number of memoized helpers being used in
practice:

10 lets https://github.com/forem/forem/blob/master/spec/support/api_analytics_shared_examples.rb#L2
20 lets https://github.com/chef/chef/blob/master/spec/unit/provider/systemd_unit_spec.rb#L23
30 lets https://github.com/chef/chef/blob/master/spec/unit/policy_builder/policyfile_spec.rb#L24
40 lets https://github.com/diaspora/diaspora/blob/develop/spec/integration/migration_service_spec.rb#L7
50 lets https://github.com/chef/chef/blob/master/spec/functional/resource/dsc_script_spec.rb#L71

Allowing `subject` by default, i.e. subjects don't count towards the
number of memoized helpers, since there is a dedicated
`RSpec/MultipleSubjects` cop. Combination with it provides infinite
possibilities to tweak the maximum allowed number of memoized helpers to
meet the project style.
ydah pushed a commit to rubocop/rubocop-rspec_rails that referenced this pull request Mar 27, 2024
Switching the cop on by default to match RSpec recommendations from its
docs and the community RSpec Style Guide recommendations.

    `let` _can_ enhance readability when used sparingly (1,2, or
    maybe 3 declarations) in any given example group, but that can
    quickly degrade with overuse. YMMV.

https://github.com/rspec/rspec-core/blob/5d87c28bb828ba14b54a0d8cfddf044894009314/lib/rspec/core/memoized_helpers.rb#L257

https://reddit.com/r/ruby/comments/73nrhk/ama_the_authors_of_effective_testing_with_rspec_3/dnsyanp/

https://rspec.rubystyle.guide/#let-blocks

Example change to dramatically reduce the number of used memoized
helpers without resorting to methods rubocop/rubocop#8447

There is no limit to the number of memoized helpers being used in
practice:

10 lets https://github.com/forem/forem/blob/master/spec/support/api_analytics_shared_examples.rb#L2
20 lets https://github.com/chef/chef/blob/master/spec/unit/provider/systemd_unit_spec.rb#L23
30 lets https://github.com/chef/chef/blob/master/spec/unit/policy_builder/policyfile_spec.rb#L24
40 lets https://github.com/diaspora/diaspora/blob/develop/spec/integration/migration_service_spec.rb#L7
50 lets https://github.com/chef/chef/blob/master/spec/functional/resource/dsc_script_spec.rb#L71

Allowing `subject` by default, i.e. subjects don't count towards the
number of memoized helpers, since there is a dedicated
`RSpec/MultipleSubjects` cop. Combination with it provides infinite
possibilities to tweak the maximum allowed number of memoized helpers to
meet the project style.
ydah pushed a commit to rubocop/rubocop-rspec_rails that referenced this pull request Mar 27, 2024
Switching the cop on by default to match RSpec recommendations from its
docs and the community RSpec Style Guide recommendations.

    `let` _can_ enhance readability when used sparingly (1,2, or
    maybe 3 declarations) in any given example group, but that can
    quickly degrade with overuse. YMMV.

https://github.com/rspec/rspec-core/blob/5d87c28bb828ba14b54a0d8cfddf044894009314/lib/rspec/core/memoized_helpers.rb#L257

https://reddit.com/r/ruby/comments/73nrhk/ama_the_authors_of_effective_testing_with_rspec_3/dnsyanp/

https://rspec.rubystyle.guide/#let-blocks

Example change to dramatically reduce the number of used memoized
helpers without resorting to methods rubocop/rubocop#8447

There is no limit to the number of memoized helpers being used in
practice:

10 lets https://github.com/forem/forem/blob/master/spec/support/api_analytics_shared_examples.rb#L2
20 lets https://github.com/chef/chef/blob/master/spec/unit/provider/systemd_unit_spec.rb#L23
30 lets https://github.com/chef/chef/blob/master/spec/unit/policy_builder/policyfile_spec.rb#L24
40 lets https://github.com/diaspora/diaspora/blob/develop/spec/integration/migration_service_spec.rb#L7
50 lets https://github.com/chef/chef/blob/master/spec/functional/resource/dsc_script_spec.rb#L71

Allowing `subject` by default, i.e. subjects don't count towards the
number of memoized helpers, since there is a dedicated
`RSpec/MultipleSubjects` cop. Combination with it provides infinite
possibilities to tweak the maximum allowed number of memoized helpers to
meet the project style.
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.

None yet

3 participants