Skip to content

Commit 0b5b672

Browse files
authored
Merge pull request #226 from rubocop-hq/dont-stub-your-mock
Add RSpec/StubbedMock cop
2 parents f1048eb + ee4f9fd commit 0b5b672

File tree

8 files changed

+347
-2
lines changed

8 files changed

+347
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* Move our documentation from rubocop-rspec.readthedocs.io to docs.rubocop.org/rubocop-rspec. ([@bquorning][])
66
* Add `RSpec/RepeatedIncludeExample` cop. ([@biinari][])
77
* Fix false positives in `RSpec/EmptyExampleGroup`. ([@pirj][])
8+
* Add `RSpec/StubbedMock` cop. ([@bquorning][], [@pirj][])
89

910
## 1.43.2 (2020-08-25)
1011

config/default.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,12 @@ RSpec/SingleArgumentMessageChain:
543543
VersionChanged: '1.10'
544544
StyleGuide: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SingleArgumentMessageChain
545545

546+
RSpec/StubbedMock:
547+
Description: Checks that message expectations do not have a configured response.
548+
Enabled: pending
549+
VersionAdded: '1.44'
550+
StyleGuide: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/StubbedMock
551+
546552
RSpec/SubjectStub:
547553
Description: Checks for stubbed test subjects.
548554
Enabled: true

docs/modules/ROOT/pages/cops.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
* xref:cops_rspec.adoc#rspecsharedcontext[RSpec/SharedContext]
8787
* xref:cops_rspec.adoc#rspecsharedexamples[RSpec/SharedExamples]
8888
* xref:cops_rspec.adoc#rspecsingleargumentmessagechain[RSpec/SingleArgumentMessageChain]
89+
* xref:cops_rspec.adoc#rspecstubbedmock[RSpec/StubbedMock]
8990
* xref:cops_rspec.adoc#rspecsubjectstub[RSpec/SubjectStub]
9091
* xref:cops_rspec.adoc#rspecunspecifiedexception[RSpec/UnspecifiedException]
9192
* xref:cops_rspec.adoc#rspecvariabledefinition[RSpec/VariableDefinition]

docs/modules/ROOT/pages/cops_rspec.adoc

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3814,6 +3814,36 @@ allow(foo).to receive("bar.baz")
38143814

38153815
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/SingleArgumentMessageChain
38163816

3817+
== RSpec/StubbedMock
3818+
3819+
|===
3820+
| Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
3821+
3822+
| Pending
3823+
| Yes
3824+
| No
3825+
| 1.44
3826+
| -
3827+
|===
3828+
3829+
Checks that message expectations do not have a configured response.
3830+
3831+
=== Examples
3832+
3833+
[source,ruby]
3834+
----
3835+
# bad
3836+
expect(foo).to receive(:bar).with(42).and_return("hello world")
3837+
3838+
# good (without spies)
3839+
allow(foo).to receive(:bar).with(42).and_return("hello world")
3840+
expect(foo).to receive(:bar).with(42)
3841+
----
3842+
3843+
=== References
3844+
3845+
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/StubbedMock
3846+
38173847
== RSpec/SubjectStub
38183848

38193849
|===

lib/rubocop/cop/rspec/stubbed_mock.rb

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module RSpec
6+
# Checks that message expectations do not have a configured response.
7+
#
8+
# @example
9+
#
10+
# # bad
11+
# expect(foo).to receive(:bar).with(42).and_return("hello world")
12+
#
13+
# # good (without spies)
14+
# allow(foo).to receive(:bar).with(42).and_return("hello world")
15+
# expect(foo).to receive(:bar).with(42)
16+
#
17+
class StubbedMock < Base
18+
MSG = 'Prefer `%<replacement>s` over `%<method_name>s` when ' \
19+
'configuring a response.'
20+
21+
# @!method message_expectation?(node)
22+
# Match message expectation matcher
23+
#
24+
# @example source that matches
25+
# receive(:foo)
26+
#
27+
# @example source that matches
28+
# receive_message_chain(:foo, :bar)
29+
#
30+
# @example source that matches
31+
# receive(:foo).with('bar')
32+
#
33+
# @param node [RuboCop::AST::Node]
34+
# @return [Array<RuboCop::AST::Node>] matching nodes
35+
def_node_matcher :message_expectation?, <<-PATTERN
36+
{
37+
(send nil? { :receive :receive_message_chain } ...) # receive(:foo)
38+
(send (send nil? :receive ...) :with ...) # receive(:foo).with('bar')
39+
}
40+
PATTERN
41+
42+
def_node_matcher :configured_response?, <<~PATTERN
43+
{ :and_return :and_raise :and_throw :and_yield
44+
:and_call_original :and_wrap_original }
45+
PATTERN
46+
47+
# @!method expectation(node)
48+
# Match expectation
49+
#
50+
# @example source that matches
51+
# is_expected.to be_in_the_bar
52+
#
53+
# @example source that matches
54+
# expect(cocktail).to contain_exactly(:fresh_orange_juice, :campari)
55+
#
56+
# @example source that matches
57+
# expect_any_instance_of(Officer).to be_alert
58+
#
59+
# @param node [RuboCop::AST::Node]
60+
# @yield [RuboCop::AST::Node] expectation, method name, matcher
61+
def_node_matcher :expectation, <<~PATTERN
62+
(send
63+
$(send nil? $#{Expectations::ALL.node_pattern_union} ...)
64+
:to $_)
65+
PATTERN
66+
67+
# @!method matcher_with_configured_response(node)
68+
# Match matcher with a configured response
69+
#
70+
# @example source that matches
71+
# receive(:foo).and_return('bar')
72+
#
73+
# @example source that matches
74+
# receive(:lower).and_raise(SomeError)
75+
#
76+
# @example source that matches
77+
# receive(:redirect).and_call_original
78+
#
79+
# @param node [RuboCop::AST::Node]
80+
# @yield [RuboCop::AST::Node] matcher
81+
def_node_matcher :matcher_with_configured_response, <<~PATTERN
82+
(send #message_expectation? #configured_response? _)
83+
PATTERN
84+
85+
# @!method matcher_with_return_block(node)
86+
# Match matcher with a return block
87+
#
88+
# @example source that matches
89+
# receive(:foo) { 'bar' }
90+
#
91+
# @param node [RuboCop::AST::Node]
92+
# @yield [RuboCop::AST::Node] matcher
93+
def_node_matcher :matcher_with_return_block, <<~PATTERN
94+
(block #message_expectation? args _) # receive(:foo) { 'bar' }
95+
PATTERN
96+
97+
# @!method matcher_with_hash(node)
98+
# Match matcher with a configured response defined as a hash
99+
#
100+
# @example source that matches
101+
# receive_messages(foo: 'bar', baz: 'qux')
102+
#
103+
# @example source that matches
104+
# receive_message_chain(:foo, bar: 'baz')
105+
#
106+
# @param node [RuboCop::AST::Node]
107+
# @yield [RuboCop::AST::Node] matcher
108+
def_node_matcher :matcher_with_hash, <<~PATTERN
109+
{
110+
(send nil? :receive_messages hash) # receive_messages(foo: 'bar', baz: 'qux')
111+
(send nil? :receive_message_chain ... hash) # receive_message_chain(:foo, bar: 'baz')
112+
}
113+
PATTERN
114+
115+
# @!method matcher_with_blockpass(node)
116+
# Match matcher with a configured response in block-pass
117+
#
118+
# @example source that matches
119+
# receive(:foo, &canned)
120+
#
121+
# @example source that matches
122+
# receive_message_chain(:foo, :bar, &canned)
123+
#
124+
# @example source that matches
125+
# receive(:foo).with('bar', &canned)
126+
#
127+
# @param node [RuboCop::AST::Node]
128+
# @yield [RuboCop::AST::Node] matcher
129+
def_node_matcher :matcher_with_blockpass, <<~PATTERN
130+
{
131+
(send nil? { :receive :receive_message_chain } ... block_pass) # receive(:foo, &canned)
132+
(send (send nil? :receive ...) :with ... block_pass) # receive(:foo).with('foo', &canned)
133+
}
134+
PATTERN
135+
136+
def on_send(node)
137+
expectation(node, &method(:on_expectation))
138+
end
139+
140+
private
141+
142+
def on_expectation(expectation, method_name, matcher)
143+
flag_expectation = lambda do
144+
add_offense(expectation, message: msg(method_name))
145+
end
146+
147+
matcher_with_configured_response(matcher, &flag_expectation)
148+
matcher_with_return_block(matcher, &flag_expectation)
149+
matcher_with_hash(matcher, &flag_expectation)
150+
matcher_with_blockpass(matcher, &flag_expectation)
151+
end
152+
153+
def msg(method_name)
154+
format(MSG,
155+
method_name: method_name,
156+
replacement: replacement(method_name))
157+
end
158+
159+
def replacement(method_name)
160+
case method_name
161+
when :expect
162+
:allow
163+
when :is_expected
164+
'allow(subject)'
165+
when :expect_any_instance_of
166+
:allow_any_instance_of
167+
end
168+
end
169+
end
170+
end
171+
end
172+
end

lib/rubocop/cop/rspec_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
require_relative 'rspec/shared_context'
8787
require_relative 'rspec/shared_examples'
8888
require_relative 'rspec/single_argument_message_chain'
89+
require_relative 'rspec/stubbed_mock'
8990
require_relative 'rspec/subject_stub'
9091
require_relative 'rspec/unspecified_exception'
9192
require_relative 'rspec/variable_definition'

spec/project/default_config_spec.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ def cop_configuration(config_key)
7070
expect(cop_configuration('Description')).to all(end_with('.'))
7171
end
7272

73-
it 'includes Enabled: true for every cop' do
74-
expect(cop_configuration('Enabled')).to all(be(true).or(be(false)))
73+
it 'includes a valid Enabled for every cop' do
74+
expect(cop_configuration('Enabled'))
75+
.to all be(true).or(be(false)).or(eq('pending'))
7576
end
7677
end
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::RSpec::StubbedMock do
4+
subject(:cop) { described_class.new }
5+
6+
it 'flags stubbed message expectation' do
7+
expect_offense(<<-RUBY)
8+
expect(foo).to receive(:bar).and_return('hello world')
9+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
10+
RUBY
11+
end
12+
13+
it 'flags stubbed message expectation with a block' do
14+
expect_offense(<<-RUBY)
15+
expect(foo).to receive(:bar) { 'hello world' }
16+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
17+
RUBY
18+
end
19+
20+
it 'flags stubbed message expectation with argument matching' do
21+
expect_offense(<<-RUBY)
22+
expect(foo).to receive(:bar).with(42).and_return('hello world')
23+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
24+
RUBY
25+
end
26+
27+
it 'flags stubbed message expectation with argument matching and a block' do
28+
expect_offense(<<-RUBY)
29+
expect(foo).to receive(:bar).with(42) { 'hello world' }
30+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
31+
RUBY
32+
end
33+
34+
it 'ignores `have_received`' do
35+
expect_no_offenses(<<-RUBY)
36+
expect(foo).to have_received(:bar)
37+
RUBY
38+
end
39+
40+
it 'flags `receive_messages`' do
41+
expect_offense(<<-RUBY)
42+
expect(foo).to receive_messages(foo: 42, bar: 777)
43+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
44+
RUBY
45+
end
46+
47+
it 'flags `receive_message_chain`' do
48+
expect_offense(<<-RUBY)
49+
expect(foo).to receive_message_chain(:foo, bar: 777)
50+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
51+
RUBY
52+
end
53+
54+
it 'flags `receive_message_chain` with `.and_return`' do
55+
expect_offense(<<-RUBY)
56+
expect(foo).to receive_message_chain(:foo, :bar).and_return(777)
57+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
58+
RUBY
59+
end
60+
61+
it 'flags `receive_message_chain` with a block' do
62+
expect_offense(<<-RUBY)
63+
expect(foo).to receive_message_chain(:foo, :bar) { 777 }
64+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
65+
RUBY
66+
end
67+
68+
it 'flags with order and count constraints', :pending do
69+
expect_offense(<<-RUBY)
70+
expect(foo).to receive(:bar) { 'hello world' }.ordered
71+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
72+
expect(foo).to receive(:bar).ordered { 'hello world' }
73+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
74+
expect(foo).to receive(:bar).with(42).ordered { 'hello world' }
75+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
76+
expect(foo).to receive(:bar).once.with(42).ordered { 'hello world' }
77+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
78+
expect(foo).to receive(:bar) { 'hello world' }.once.with(42).ordered
79+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
80+
expect(foo).to receive(:bar).once.with(42).and_return('hello world').ordered
81+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
82+
RUBY
83+
end
84+
85+
it 'flags block-pass' do
86+
expect_offense(<<-RUBY)
87+
canned = -> { 42 }
88+
expect(foo).to receive(:bar, &canned)
89+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
90+
expect(foo).to receive(:bar).with(42, &canned)
91+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
92+
expect(foo).to receive_message_chain(:foo, :bar, &canned)
93+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
94+
RUBY
95+
end
96+
97+
it 'flags `is_expected`' do
98+
expect_offense(<<~RUBY)
99+
is_expected.to receive(:bar).and_return(:baz)
100+
^^^^^^^^^^^ Prefer `allow(subject)` over `is_expected` when configuring a response.
101+
RUBY
102+
end
103+
104+
it 'flags `expect_any_instance_of`' do
105+
expect_offense(<<~RUBY)
106+
expect_any_instance_of(Foo).to receive(:bar).and_return(:baz)
107+
^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `allow_any_instance_of` over `expect_any_instance_of` when configuring a response.
108+
RUBY
109+
end
110+
111+
it 'ignores message allowances' do
112+
expect_no_offenses(<<-RUBY)
113+
allow(foo).to receive(:bar).and_return('hello world')
114+
allow(foo).to receive(:bar) { 'hello world' }
115+
allow(foo).to receive(:bar).with(42).and_return('hello world')
116+
allow(foo).to receive(:bar).with(42) { 'hello world' }
117+
allow(foo).to receive_messages(foo: 42, bar: 777)
118+
allow(foo).to receive_message_chain(:foo, bar: 777)
119+
allow(foo).to receive_message_chain(:foo, :bar).and_return(777)
120+
allow(foo).to receive_message_chain(:foo, :bar) { 777 }
121+
allow(foo).to receive(:bar, &canned)
122+
RUBY
123+
end
124+
125+
it 'tolerates passed arguments without parentheses' do
126+
expect_offense(<<-RUBY)
127+
expect(Foo)
128+
^^^^^^^^^^^ Prefer `allow` over `expect` when configuring a response.
129+
.to receive(:new)
130+
.with(bar).and_return baz
131+
RUBY
132+
end
133+
end

0 commit comments

Comments
 (0)