Skip to content

Commit e6c5321

Browse files
authored
Merge pull request #80 from rubocop/72
Add new `RSpecRails/ReceivePerformLater` cop
2 parents 41b6f6f + c2f63d6 commit e6c5321

File tree

7 files changed

+618
-0
lines changed

7 files changed

+618
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Fix a false positive for `RspecRails/NegationBeValid` when use `to_not`. ([@ydah])
66
- Supporting correcting `assest_redirected_to` in `RSpec/Rails/MinitestAssertions`. ([@nzlaura])
7+
- Add new `RSpecRails/ReceivePerformLater` cop. ([@ydah])
78

89
## 2.32.0 (2025-11-12)
910

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
@@ -455,6 +455,60 @@ expect(foo).to be_invalid.or be_even
455455
456456
* https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/NegationBeValid
457457
458+
[#rspecrailsreceiveperformlater]
459+
== RSpecRails/ReceivePerformLater
460+
461+
|===
462+
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed
463+
464+
| Pending
465+
| Yes
466+
| No
467+
| 2.33
468+
| -
469+
|===
470+
471+
Prefer `have_enqueued_job` over `receive(:perform_later)`.
472+
473+
The `have_enqueued_job` matcher is preferred for testing ActiveJob
474+
enqueuing. It is more explicit and provides better clarity than
475+
using `receive(:perform_later)`.
476+
477+
[#examples-rspecrailsreceiveperformlater]
478+
=== Examples
479+
480+
[source,ruby]
481+
----
482+
# bad
483+
expect(MyJob).to receive(:perform_later)
484+
do_something
485+
486+
# bad
487+
allow(MyJob).to receive(:perform_later)
488+
do_something
489+
expect(MyJob).to have_received(:perform_later)
490+
491+
# bad
492+
expect(MyJob).to receive(:perform_later).with(user, order)
493+
494+
# good
495+
expect { do_something }.to have_enqueued_job(MyJob)
496+
497+
# good
498+
expect { do_something }.to have_enqueued_job(MyJob).with(user, order)
499+
500+
# good
501+
expect { do_something }
502+
.to have_enqueued_job(MyJob)
503+
.on_queue('mailers')
504+
.at(Date.tomorrow.noon)
505+
----
506+
507+
[#references-rspecrailsreceiveperformlater]
508+
=== References
509+
510+
* https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/ReceivePerformLater
511+
458512
[#rspecrailstravelaround]
459513
== RSpecRails/TravelAround
460514
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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+
RUNNERS = %i[to to_not not_to].freeze
44+
45+
# @!method receive_perform_later?(node)
46+
def_node_matcher :receive_perform_later?, <<~PATTERN
47+
(send nil? {:receive :have_received}
48+
(sym :perform_later))
49+
PATTERN
50+
51+
# @!method expect_or_allow?(node)
52+
def_node_matcher :expect_or_allow?, <<~PATTERN
53+
(send nil? {:expect :allow} const_type?)
54+
PATTERN
55+
56+
def on_send(node)
57+
return unless receive_perform_later?(node)
58+
return unless (runner_node = find_runner_node(node))
59+
60+
expect_node = runner_node.receiver
61+
return unless expect_or_allow?(expect_node)
62+
return if allow_receive_combination?(expect_node, node)
63+
64+
job_class = expect_node.first_argument
65+
offense_node = find_offense_range(runner_node)
66+
add_offense(offense_node,
67+
message: offense_message(expect_node, job_class,
68+
runner_node, node))
69+
end
70+
71+
private
72+
73+
def allow_receive_combination?(expect_node, matcher_node)
74+
expect_node.method?(:allow) && matcher_node.method?(:receive)
75+
end
76+
77+
def offense_message(expect_node, job_class, runner_node, matcher_node)
78+
format(MSG,
79+
receiver: expect_node.method_name,
80+
job_class: job_class.source,
81+
to: runner_node.method_name,
82+
matcher: matcher_node.method_name)
83+
end
84+
85+
def find_runner_node(node)
86+
node.each_ancestor(:send).find { |ancestor| runner?(ancestor) }
87+
end
88+
89+
def find_offense_range(runner_node)
90+
current = runner_node
91+
current = current.parent while chained_send?(current)
92+
current
93+
end
94+
95+
def chained_send?(node)
96+
node.parent&.send_type? && node.parent.receiver == node
97+
end
98+
99+
def runner?(node)
100+
RUNNERS.include?(node.method_name)
101+
end
102+
end
103+
end
104+
end
105+
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'

0 commit comments

Comments
 (0)