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

Don’t stub when you’re mocking #226

Merged
merged 2 commits into from Aug 25, 2020
Merged

Don’t stub when you’re mocking #226

merged 2 commits into from Aug 25, 2020

Conversation

bquorning
Copy link
Collaborator

@bquorning bquorning commented Oct 19, 2016

Fixes #221.

TODO

  • Must also work for and_raise, and_yield, and_throw, and_call_original, and and_wrap_original.
  • Changelog entry.
  • Rename cop?
  • Better cop description.

cc @backus

If you have created a new cop:

  • Added the new cop to config/default.yml.
  • The cop documents examples of good and bad code.
  • The tests assert both that bad code is reported and that good code is not reported.
  • Set VersionAdded in default/config.yml to the next minor version.

@bquorning bquorning force-pushed the dont-stub-your-mock branch 2 times, most recently from 1cbad46 to ce9cd8d Compare October 19, 2016 19:41
@backus
Copy link
Collaborator

backus commented Oct 24, 2016

@bquorning I played with the branch locally to get the highlighting you are interested in. This should work:

diff --git i/lib/rubocop/cop/rspec/mock_not_stub.rb w/lib/rubocop/cop/rspec/mock_not_stub.rb
index 286df07..aba2985 100644
--- i/lib/rubocop/cop/rspec/mock_not_stub.rb
+++ w/lib/rubocop/cop/rspec/mock_not_stub.rb
@@ -27,8 +27,8 @@ module RuboCop
           (send
             (send nil :expect ...) :to
             {
-              (send #receive :and_return $_)
-              (block #receive (args) $_)
+              $(send #receive :and_return _)
+              $(block #receive (args) _)
             }
           )
         PATTERN
@@ -42,7 +42,24 @@ module RuboCop

         def on_send(node)
           message_expectation_with_return_block(node) do |match|
-            add_offense(match, :expression)
+            source_map = match.loc
+
+            offending_range =
+              if match.send_type?
+                Parser::Source::Range.new(
+                  source_map.expression.source_buffer,
+                  source_map.selector.begin_pos,
+                  source_map.end.end_pos
+                )
+              else
+                Parser::Source::Range.new(
+                  source_map.expression.source_buffer,
+                  source_map.begin.begin_pos,
+                  source_map.end.end_pos
+                )
+              end
+
+            add_offense(match, offending_range)
           end
         end
       end

I'd refactor it a bit of course to make it not just one big method.

@backus
Copy link
Collaborator

backus commented Oct 24, 2016

Nice job with the node pattern stuff btw. I wish people would use it more on rubocop proper

@bquorning
Copy link
Collaborator Author

Thanks for the code snippet there, @backus. I think I’ll use source_map.dot.begin_pos instead of source_map.selector.begin_pos and get highlight on the . preceding and_return as well.

config/default.yml Outdated Show resolved Hide resolved
@backus
Copy link
Collaborator

backus commented Oct 24, 2016

Should this cop be configurable? It seems like expect(...).to receive(...).and_return(...) should be a valid style.

@bquorning
Copy link
Collaborator Author

Configurable? You can just disable it. Perhaps I misunderstand you?

@backus
Copy link
Collaborator

backus commented Oct 24, 2016

No I guess I was curious if there was an alternate style that this sort of thing could also enforce if we're just saying "do not use this part of the RSpec API."

I look forward to hearing outside input for @jcoyne. I'm wondering how users will respond to this cop.

@backus
Copy link
Collaborator

backus commented Oct 24, 2016

Should this cop also reject things like expect(...).to receive(...).and_raise(...), expect(...).to receive(...).and_yield(...), expect(...).to receive(...).and_throw(...)?

@bquorning
Copy link
Collaborator Author

Should this cop also reject things like …

Yes, good point. All three cases should be policed by this cop.

@bquorning
Copy link
Collaborator Author

Also and_call_original, and_wrap_original, yield_with_args

@jcoyne
Copy link

jcoyne commented Oct 24, 2016

Can you say why this is good style? What's the justification?

@bquorning
Copy link
Collaborator Author

The argumentation / justification is in #221.

@jcoyne
Copy link

jcoyne commented Oct 24, 2016

It sounds like you want to change the rspec-mocks API via rubocop. Have you considered this change as a deprecation in rspec-mocks?

@bquorning
Copy link
Collaborator Author

Have you considered this change as a deprecation in rspec-mocks?

Yes :-) I thought it would be easier to start here.

I’ve also considered mentioning someone from rspec-mocks in this PR.

@jcoyne
Copy link

jcoyne commented Oct 24, 2016

@bquorning I think that would be a good idea. If you want to influence the usage of a library, it makes sense to consult the authors thereof.

@bquorning
Copy link
Collaborator Author

After a brief discussion in rspec/rspec-mocks#1123 I’m convinced that rspec-mocks will not change its DSL. Which makes this a personal style preference – perhaps suitable for RuboCop-RSpec?

@backus
Copy link
Collaborator

backus commented Oct 25, 2016

@JonRowe would you consider deprecating this behavior?

Maybe if there was a broad community consensus. But users like to use different parts of RSpec in different ways and I would expect a lot of push back on such a deprecation. In general, we don't deprecate or disallow things just because something is a code smell.

I think seeing myron call this a code smell is more than enough to convince me that this is worth making a cop

@backus
Copy link
Collaborator

backus commented Oct 25, 2016

I'll try to do another review tonight

@JonRowe
Copy link

JonRowe commented Oct 25, 2016

Just a personal opinion (which may or may not be shared with my other rspec team members) but this cop should be disabled by default it's very much a personal opinion that creates extra code for people, and rubocop tends to give the impression that something is "recommended best practise" which this isn't.

@backus
Copy link
Collaborator

backus commented Oct 25, 2016

@JonRowe thanks I'll keep that in mind. If you have a minute, would you be able to look at the other cops and say whether you think this is true for the others? I'm curious how broad you view things as style vs. best practice.

On an unrelated note, I'm not sure how much rubocop is "recommended best practice" anymore given that most cops with more than one possible style tend to be configurable in different ways.

@JonRowe
Copy link

JonRowe commented Oct 25, 2016

On an unrelated note, I'm not sure how much rubocop is "recommended best practice" anymore given that most cops with more than one possible style tend to be configurable in different ways.

I see people using the default config a fair bit, and just accepting them as "the way"

@bquorning bquorning force-pushed the dont-stub-your-mock branch 3 times, most recently from 5ae0d4f to 3ab2326 Compare January 1, 2019 21:42
Copy link
Member

@pirj pirj left a comment

Choose a reason for hiding this comment

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

Looks good.

lib/rubocop/cop/rspec/mock_not_stub.rb Outdated Show resolved Hide resolved
lib/rubocop/cop/rspec/mock_not_stub.rb Outdated Show resolved Hide resolved
@dgollahon
Copy link
Contributor

I kind of lean with the disable-by-default crowd here. This feels a little obscure/confusing to me, though @bquorning motivating example in the linked issue seems reasonable and I would probably write the specs the same way he would. I strongly suspect we'll get very confused users asking what we mean by this cop though and why it's better to do it another way, so my intuition is that it's a little too surgical for the average user.

If this gets merged and enabled by default, the explanation/motivation should probably be documented more clearly, imo.

@pirj
Copy link
Member

pirj commented Aug 22, 2020

@Darhazer @bquorning Rebased, ready for reviews. If you have objections to merging this, please speak up.

@pirj
Copy link
Member

pirj commented Aug 24, 2020

Even though it's your own pull request, I'd love to hear your thoughts on the recent changes before merging it @bquorning

@pirj pirj force-pushed the dont-stub-your-mock branch 3 times, most recently from 8915b31 to 7906f23 Compare August 24, 2020 20:09
lib/rubocop/cop/rspec/stubbed_mock.rb Outdated Show resolved Hide resolved
lib/rubocop/cop/rspec/stubbed_mock.rb Outdated Show resolved Hide resolved

private

def on_expectation(expectation, method_name, matcher)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe I’m having a bad day, but I have a really hard time understanding this code. It’s not immediately obvious what the node matchers actually match. The names are easy enough to understand (maybe except matcher_with_blockpass_or_hash which I’m not sure I get), but… Maybe we should start adding adding comments to our node matchers, with one or two examples of what the would match?

It can also be that I lack training in reading node patterns (and parser output). I wonder how others feel about this?

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for this fresh view on the cop.
It's quite common to miss the complexity when working on it, but as time goes, this complexity becomes apparent.

In addition to node matcher YARD docs that I believe now help a lot to understand RSpec/EmptyExampleGroup's node patterns, there's also Node pattern comment feature.
I intend to add both to this cop's node patterns.

spec/rubocop/cop/rspec/stubbed_mock_spec.rb Outdated Show resolved Hide resolved
@bquorning
Copy link
Collaborator Author

bquorning commented Aug 25, 2020

Even though it's your own pull request, I'd love to hear your thoughts on the recent changes before merging it @bquorning

Absolutely. Sorry for sometimes taking several days before responding; it feels like there are fewer hours in a day than there used to be 😕

Perhaps the spec for pending state can stand alone, but you can squash the other commits (just change me to be co-author).

Co-authored-by: Phil Pirozhkov <hello@fili.pp.ru>
def_node_matcher :matcher_with_blockpass, <<~PATTERN
{
(send nil? { :receive :receive_message_chain } ... block_pass) # receive(:foo, &canned)
(send (send nil? :receive ...) :with ... block_pass) # receive(:foo).with('foo', &canned)
Copy link
Member

Choose a reason for hiding this comment

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

That's kind of odd that LineLength ignores HEREDOC

@pirj pirj merged commit 0b5b672 into master Aug 25, 2020
@pirj pirj deleted the dont-stub-your-mock branch August 25, 2020 22:35
def_node_matcher :matcher_with_blockpass, <<~PATTERN
{
(send nil? { :receive :receive_message_chain } ... block_pass) # receive(:foo, &canned)
(send (send nil? :receive ...) :with ... block_pass) # receive(:foo).with('foo', &canned)
Copy link
Member

Choose a reason for hiding this comment

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

Wondering if you guys find that kind of node pattern comments helpful @bquorning @Darhazer

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I found them very useful, thank you @pirj

@marcandre
Copy link
Contributor

marcandre commented Nov 6, 2020

# bad
expect(foo).to receive(:bar).with(42).and_return("hello world")
# good (without spies)
allow(foo).to receive(:bar).with(42).and_return("hello world")
expect(foo).to receive(:bar).with(42)

I strongly disagree that an expressive and DRY form should be broken into two repetitive forms for no benefit. I would support a cop that does the reverse.

I really dislike when cops complain about valid code, especially when the solution is not clearly better (i.e. clearly worse for many, me included).

Of course, I fully realize that yes, we can disable it. That mere fact does not imply that the cop should be enabled by default.

I believe having cops like these, enabled by default, give RuboCop a bad name.

Note: I'm sorry, I didn't read the whole thread as it contains comments on the code as well as discussion about the usefulness of the cop itself. I'll just comment on this:

I think seeing myron call this a code smell is more than enough to convince me that this is worth making a cop

He did not specifically call this a code smell (and I would disagree with him if he did)

marcandre added a commit to marcandre/rubocop-ast that referenced this pull request Nov 6, 2020
@bquorning
Copy link
Collaborator Author

There has been a lot of discussion for and against whether this cop should be enabled by default. I am actually not sure if we really made any decision either way.

@pirj I remember you revived this PR in #226 (comment), but I do not recall if we really decided to let this cop be enabled or disabled by default. I think it just … happened.

marcandre added a commit to rubocop/rubocop-ast that referenced this pull request Nov 6, 2020
marcandre added a commit to marcandre/rubocop that referenced this pull request Nov 6, 2020
@marcandre
Copy link
Contributor

I think it just … happened.

Maybe disabling it and issuing a 2.0.1 release is the way to go then.

marcandre added a commit to rubocop/rubocop that referenced this pull request Nov 6, 2020
@pirj
Copy link
Member

pirj commented Nov 7, 2020

@marcandre

~/source/rubocop $ rubocop --only RSpec/StubbedMock

Offenses:

spec/rubocop/result_cache_spec.rb:178:11: C: RSpec/StubbedMock: Prefer allow over expect when configuring a response.
          expect(team).to(
          ^^^^^^^^^^^^
spec/rubocop/result_cache_spec.rb:191:11: C: RSpec/StubbedMock: Prefer allow over expect when configuring a response.
          expect(team).to(
          ^^^^^^^^^^^^
spec/rubocop/result_cache_spec.rb:332:9: C: RSpec/StubbedMock: Prefer allow over expect when configuring a response.
        expect(FileUtils).to receive(:mkdir_p).with(start_with(cache_root))
        ^^^^^^^^^^^^^^^^^

... 3 offenses detected

Let's take a closer look:

      context 'when team external_dependency_checksum changes' do
        it 'is invalid' do
          cache.save(offenses)
          expect(team).to(
            receive(:external_dependency_checksum).and_return('bar')
          )
          cache2 = described_class.new(
            file, team, options, config_store, cache_root
          )
          expect(cache2.valid?).to eq(false)
        end
      end

If I would be reading this, I would ask myself:

  1. Why is expect(team) an expect and not allow, is there a risk that the SUT won't send an external_dependency_checksum message to team?
  2. Why, if we have:
  • properly balanced examples
  • we check the output

we didn't limit ourselves to set the input and rather decided to increase specs' brittleness by tying ourselves to the internal implementation?

You may also notice that:

  before do
    #...
    allow(team).to receive(:external_dependency_checksum).and_return('foo')`

This means we're double-stubbing external_dependency_checksum 😱
We should have received RSpec's:

            "You're overriding a previous stub implementation of `#{@message}`. " \
            "Called from #{CallerFilter.first_non_rspec_line}."

warning, but we don't, not sure why exactly, probably because it's only implemented for pure doubles and doesn't work on partial doubles, or warnings are muted somehow.

The fact that this spec works doesn't mean it's good. I can lend you a hand with fixing it and turning StubbedMock back to Enabled: true if you like.

I don't have other specs on hand to reason about, but I'm all ears to know when RSpec/StubbedMock doesn't make sense at all.

@marcandre
Copy link
Contributor

marcandre commented Nov 7, 2020

we're double-stubbing external_dependency_checksum

The first allow is called once to build the cache, then the method is stubbed again for the second call to check the cache validity.

increase specs' brittleness

The spec assumes an implementation detail. If that implementation detail, the spec needs to be changed. That's why we stub only when there is no better alternative. I see the spec failing as a virtue.

Other remarks in rubocop/rubocop#9006

@pirj
Copy link
Member

pirj commented Nov 8, 2020

Just for the record, I take back my argument that double-stubbing is an offence, it's my false belief.

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.

Cop idea: Don’t stub when you’re mocking
8 participants