Skip to content

Commit dc79d2b

Browse files
committed
Add new RSpecRails/ReceivePerformLater cop
Fixes: #72
1 parent 0ecefbc commit dc79d2b

File tree

7 files changed

+324
-0
lines changed

7 files changed

+324
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Master (Unreleased)
44

5+
- Add new `RSpecRails/ReceivePerformLater` cop. ([@ydah])
6+
57
## 2.32.0 (2025-11-12)
68

79
- Add `RSpecRails/HttpStatusNameConsistency` cop. ([@taketo1113])

config/default.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ RSpecRails/NegationBeValid:
8383
VersionChanged: '2.29'
8484
Reference: https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/NegationBeValid
8585

86+
RSpecRails/ReceivePerformLater:
87+
Description: Prefer `have_enqueued_job` over `receive(:perform_later)`.
88+
Enabled: pending
89+
VersionAdded: '2.33'
90+
Reference: https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/ReceivePerformLater
91+
8692
RSpecRails/TravelAround:
8793
Description: Prefer to travel in `before` rather than `around`.
8894
Enabled: pending

docs/modules/ROOT/pages/cops.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* xref:cops_rspecrails.adoc#rspecrailsinferredspectype[RSpecRails/InferredSpecType]
1010
* xref:cops_rspecrails.adoc#rspecrailsminitestassertions[RSpecRails/MinitestAssertions]
1111
* xref:cops_rspecrails.adoc#rspecrailsnegationbevalid[RSpecRails/NegationBeValid]
12+
* xref:cops_rspecrails.adoc#rspecrailsreceiveperformlater[RSpecRails/ReceivePerformLater]
1213
* xref:cops_rspecrails.adoc#rspecrailstravelaround[RSpecRails/TravelAround]
1314

1415
// END_COP_LIST

docs/modules/ROOT/pages/cops_rspecrails.adoc

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,60 @@ expect(foo).to be_invalid.or be_even
453453
454454
* https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/NegationBeValid
455455
456+
[#rspecrailsreceiveperformlater]
457+
== RSpecRails/ReceivePerformLater
458+
459+
|===
460+
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed
461+
462+
| Pending
463+
| Yes
464+
| No
465+
| 2.33
466+
| -
467+
|===
468+
469+
Prefer `have_enqueued_job` over `receive(:perform_later)`.
470+
471+
The `have_enqueued_job` matcher is preferred for testing ActiveJob
472+
enqueuing. It is more explicit and provides better test clarity than
473+
using `receive(:perform_later)`.
474+
475+
[#examples-rspecrailsreceiveperformlater]
476+
=== Examples
477+
478+
[source,ruby]
479+
----
480+
# bad
481+
expect(MyJob).to receive(:perform_later)
482+
do_something
483+
484+
# bad
485+
allow(MyJob).to receive(:perform_later)
486+
do_something
487+
expect(MyJob).to have_received(:perform_later)
488+
489+
# bad
490+
expect(MyJob).to receive(:perform_later).with(user, order)
491+
492+
# good
493+
expect { do_something }.to have_enqueued_job(MyJob)
494+
495+
# good
496+
expect { do_something }.to have_enqueued_job(MyJob).with(user, order)
497+
498+
# good
499+
expect { do_something }
500+
.to have_enqueued_job(MyJob)
501+
.on_queue('mailers')
502+
.at(Date.tomorrow.noon)
503+
----
504+
505+
[#references-rspecrailsreceiveperformlater]
506+
=== References
507+
508+
* https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/ReceivePerformLater
509+
456510
[#rspecrailstravelaround]
457511
== RSpecRails/TravelAround
458512
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module RSpecRails
6+
# Prefer `have_enqueued_job` over `receive(:perform_later)`.
7+
#
8+
# The `have_enqueued_job` matcher is preferred for testing ActiveJob
9+
# enqueuing. It is more explicit and provides better clarity than
10+
# using `receive(:perform_later)`.
11+
#
12+
# @example
13+
# # bad
14+
# expect(MyJob).to receive(:perform_later)
15+
# do_something
16+
#
17+
# # bad
18+
# allow(MyJob).to receive(:perform_later)
19+
# do_something
20+
# expect(MyJob).to have_received(:perform_later)
21+
#
22+
# # bad
23+
# expect(MyJob).to receive(:perform_later).with(user, order)
24+
#
25+
# # good
26+
# expect { do_something }.to have_enqueued_job(MyJob)
27+
#
28+
# # good
29+
# expect { do_something }.to have_enqueued_job(MyJob).with(user, order)
30+
#
31+
# # good
32+
# expect { do_something }
33+
# .to have_enqueued_job(MyJob)
34+
# .on_queue('mailers')
35+
# .at(Date.tomorrow.noon)
36+
#
37+
class ReceivePerformLater < ::RuboCop::Cop::Base
38+
MSG = 'Prefer `expect { ... }.to have_enqueued_job(%<job_class>s)` ' \
39+
'over `%<receiver>s(%<job_class>s).%<to>s ' \
40+
'%<matcher>s(:perform_later)`.'
41+
42+
RESTRICT_ON_SEND = %i[receive have_received].to_set
43+
EXPECT_METHODS = %i[expect allow].freeze
44+
RUNNERS = %i[to to_not not_to].freeze
45+
46+
# @!method receive_perform_later?(node)
47+
def_node_matcher :receive_perform_later?, <<~PATTERN
48+
(send nil? {:receive :have_received}
49+
(sym :perform_later))
50+
PATTERN
51+
52+
def on_send(node)
53+
return unless receive_perform_later?(node)
54+
return unless (to_node = find_to_node(node))
55+
56+
expect_node = to_node.receiver
57+
return unless expect_node?(expect_node)
58+
59+
job_class = expect_node.first_argument
60+
return unless valid_job_class?(job_class)
61+
return if allowed_combination?(expect_node, node)
62+
63+
add_offense(to_node,
64+
message: build_message(expect_node, job_class, to_node,
65+
node))
66+
end
67+
68+
private
69+
70+
def expect_node?(node)
71+
node&.send_type? && EXPECT_METHODS.include?(node.method_name)
72+
end
73+
74+
def valid_job_class?(node)
75+
node&.const_type?
76+
end
77+
78+
def allowed_combination?(expect_node, matcher_node)
79+
expect_node.method?(:allow) && matcher_node.method?(:receive)
80+
end
81+
82+
def build_message(expect_node, job_class, to_node, matcher_node)
83+
format(MSG,
84+
receiver: expect_node.method_name,
85+
job_class: job_class.source,
86+
to: to_node.method_name,
87+
matcher: matcher_node.method_name)
88+
end
89+
90+
def find_to_node(node)
91+
parent = node.parent
92+
return unless parent&.send_type?
93+
94+
return parent if runner?(parent)
95+
if parent.parent&.send_type? && runner?(parent.parent)
96+
return parent.parent
97+
end
98+
99+
nil
100+
end
101+
102+
def runner?(node)
103+
RUNNERS.include?(node.method_name)
104+
end
105+
end
106+
end
107+
end
108+
end

lib/rubocop/cop/rspec_rails_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
require_relative 'rspec_rails/inferred_spec_type'
88
require_relative 'rspec_rails/minitest_assertions'
99
require_relative 'rspec_rails/negation_be_valid'
10+
require_relative 'rspec_rails/receive_perform_later'
1011
require_relative 'rspec_rails/travel_around'
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::RSpecRails::ReceivePerformLater do
4+
context 'when using expect with receive(:perform_later)' do
5+
it 'registers an offense' do
6+
expect_offense(<<~RUBY)
7+
it 'enqueues a job' do
8+
expect(MyJob).to receive(:perform_later)
9+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to receive(:perform_later)`.
10+
do_something
11+
end
12+
RUBY
13+
end
14+
15+
it 'registers an offense with not_to' do
16+
expect_offense(<<~RUBY)
17+
it 'does not enqueue a job' do
18+
expect(MyJob).not_to receive(:perform_later)
19+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).not_to receive(:perform_later)`.
20+
do_something
21+
end
22+
RUBY
23+
end
24+
25+
it 'registers an offense with to_not' do
26+
expect_offense(<<~RUBY)
27+
it 'does not enqueue a job' do
28+
expect(MyJob).to_not receive(:perform_later)
29+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to_not receive(:perform_later)`.
30+
do_something
31+
end
32+
RUBY
33+
end
34+
end
35+
36+
context 'when using expect with receive(:perform_later).with()' do
37+
it 'registers an offense for receive with arguments' do
38+
expect_offense(<<~RUBY)
39+
it 'enqueues a job with arguments' do
40+
expect(MyJob).to receive(:perform_later).with(user, order)
41+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to receive(:perform_later)`.
42+
do_something
43+
end
44+
RUBY
45+
end
46+
47+
it 'registers an offense for receive with keyword arguments' do
48+
expect_offense(<<~RUBY)
49+
it 'enqueues a job with keyword arguments' do
50+
expect(MyJob).to receive(:perform_later).with(user_id: 1)
51+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to receive(:perform_later)`.
52+
do_something
53+
end
54+
RUBY
55+
end
56+
end
57+
58+
context 'when using allow with have_received(:perform_later)' do
59+
it 'registers an offense' do
60+
expect_offense(<<~RUBY)
61+
it 'enqueues a job' do
62+
allow(MyJob).to receive(:perform_later)
63+
do_something
64+
expect(MyJob).to have_received(:perform_later)
65+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to have_received(:perform_later)`.
66+
end
67+
RUBY
68+
end
69+
70+
it 'registers an offense with arguments' do
71+
expect_offense(<<~RUBY)
72+
it 'enqueues a job with arguments' do
73+
allow(MyJob).to receive(:perform_later)
74+
do_something
75+
expect(MyJob).to have_received(:perform_later).with(user)
76+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to have_received(:perform_later)`.
77+
end
78+
RUBY
79+
end
80+
end
81+
82+
context 'when using allow with receive(:perform_later)' do
83+
it 'does not register an offense for allow alone' do
84+
expect_no_offenses(<<~RUBY)
85+
it 'allows job enqueuing' do
86+
allow(MyJob).to receive(:perform_later)
87+
do_something
88+
end
89+
RUBY
90+
end
91+
end
92+
93+
context 'when using have_enqueued_job matcher' do
94+
it 'does not register an offense' do
95+
expect_no_offenses(<<~RUBY)
96+
it 'enqueues a job' do
97+
expect { do_something }.to have_enqueued_job(MyJob)
98+
end
99+
RUBY
100+
end
101+
102+
it 'does not register an offense with arguments' do
103+
expect_no_offenses(<<~RUBY)
104+
it 'enqueues a job with arguments' do
105+
expect { do_something }.to have_enqueued_job(MyJob).with(user, order)
106+
end
107+
RUBY
108+
end
109+
110+
it 'does not register an offense with queue and timing' do
111+
expect_no_offenses(<<~RUBY)
112+
it 'enqueues a job with options' do
113+
expect { do_something }
114+
.to have_enqueued_job(MyJob)
115+
.on_queue('mailers')
116+
.at(Date.tomorrow.noon)
117+
end
118+
RUBY
119+
end
120+
end
121+
122+
context 'when using receive with other methods' do
123+
it 'does not register an offense for receive(:perform_now)' do
124+
expect_no_offenses(<<~RUBY)
125+
it 'performs a job' do
126+
expect(MyJob).to receive(:perform_now)
127+
do_something
128+
end
129+
RUBY
130+
end
131+
132+
it 'does not register an offense for receive(:some_method)' do
133+
expect_no_offenses(<<~RUBY)
134+
it 'calls some method' do
135+
expect(MyJob).to receive(:some_method)
136+
do_something
137+
end
138+
RUBY
139+
end
140+
end
141+
142+
context 'when using receive on non-job objects' do
143+
it 'does not register an offense for instance methods' do
144+
expect_no_offenses(<<~RUBY)
145+
it 'receives a method' do
146+
expect(instance).to receive(:perform_later)
147+
instance.perform_later
148+
end
149+
RUBY
150+
end
151+
end
152+
end

0 commit comments

Comments
 (0)