Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Master (Unreleased)

- Add new `RSpecRails/ReceivePerformLater` cop. ([@ydah])

## 2.32.0 (2025-11-12)

- Add `RSpecRails/HttpStatusNameConsistency` cop. ([@taketo1113])
Expand Down
6 changes: 6 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ RSpecRails/NegationBeValid:
VersionChanged: '2.29'
Reference: https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/NegationBeValid

RSpecRails/ReceivePerformLater:
Description: Prefer `have_enqueued_job` over `receive(:perform_later)`.
Enabled: pending
VersionAdded: '2.33'
Reference: https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/ReceivePerformLater

RSpecRails/TravelAround:
Description: Prefer to travel in `before` rather than `around`.
Enabled: pending
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/cops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* xref:cops_rspecrails.adoc#rspecrailsinferredspectype[RSpecRails/InferredSpecType]
* xref:cops_rspecrails.adoc#rspecrailsminitestassertions[RSpecRails/MinitestAssertions]
* xref:cops_rspecrails.adoc#rspecrailsnegationbevalid[RSpecRails/NegationBeValid]
* xref:cops_rspecrails.adoc#rspecrailsreceiveperformlater[RSpecRails/ReceivePerformLater]
* xref:cops_rspecrails.adoc#rspecrailstravelaround[RSpecRails/TravelAround]

// END_COP_LIST
54 changes: 54 additions & 0 deletions docs/modules/ROOT/pages/cops_rspecrails.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,60 @@ expect(foo).to be_invalid.or be_even

* https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/NegationBeValid

[#rspecrailsreceiveperformlater]
== RSpecRails/ReceivePerformLater

|===
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed

| Pending
| Yes
| No
| 2.33
| -
|===

Prefer `have_enqueued_job` over `receive(:perform_later)`.

The `have_enqueued_job` matcher is preferred for testing ActiveJob
enqueuing. It is more explicit and provides better clarity than
using `receive(:perform_later)`.

[#examples-rspecrailsreceiveperformlater]
=== Examples

[source,ruby]
----
# bad
expect(MyJob).to receive(:perform_later)
do_something

# bad
allow(MyJob).to receive(:perform_later)
do_something
expect(MyJob).to have_received(:perform_later)

# bad
expect(MyJob).to receive(:perform_later).with(user, order)

# good
expect { do_something }.to have_enqueued_job(MyJob)

# good
expect { do_something }.to have_enqueued_job(MyJob).with(user, order)

# good
expect { do_something }
.to have_enqueued_job(MyJob)
.on_queue('mailers')
.at(Date.tomorrow.noon)
----

[#references-rspecrailsreceiveperformlater]
=== References

* https://www.rubydoc.info/gems/rubocop-rspec_rails/RuboCop/Cop/RSpecRails/ReceivePerformLater

[#rspecrailstravelaround]
== RSpecRails/TravelAround

Expand Down
105 changes: 105 additions & 0 deletions lib/rubocop/cop/rspec_rails/receive_perform_later.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# frozen_string_literal: true

module RuboCop
module Cop
module RSpecRails
# Prefer `have_enqueued_job` over `receive(:perform_later)`.
#
# The `have_enqueued_job` matcher is preferred for testing ActiveJob
# enqueuing. It is more explicit and provides better clarity than
# using `receive(:perform_later)`.
#
# @example
# # bad
# expect(MyJob).to receive(:perform_later)
# do_something
#
# # bad
# allow(MyJob).to receive(:perform_later)
# do_something
# expect(MyJob).to have_received(:perform_later)
#
# # bad
# expect(MyJob).to receive(:perform_later).with(user, order)
#
# # good
# expect { do_something }.to have_enqueued_job(MyJob)
#
# # good
# expect { do_something }.to have_enqueued_job(MyJob).with(user, order)
#
# # good
# expect { do_something }
# .to have_enqueued_job(MyJob)
# .on_queue('mailers')
# .at(Date.tomorrow.noon)
#
class ReceivePerformLater < ::RuboCop::Cop::Base
MSG = 'Prefer `expect { ... }.to have_enqueued_job(%<job_class>s)` ' \
'over `%<receiver>s(%<job_class>s).%<to>s ' \
'%<matcher>s(:perform_later)`.'

RESTRICT_ON_SEND = %i[receive have_received].to_set
RUNNERS = %i[to to_not not_to].freeze

# @!method receive_perform_later?(node)
def_node_matcher :receive_perform_later?, <<~PATTERN
(send nil? {:receive :have_received}
(sym :perform_later))
PATTERN

# @!method expect_or_allow?(node)
def_node_matcher :expect_or_allow?, <<~PATTERN
(send nil? {:expect :allow} const_type?)
PATTERN

def on_send(node)
return unless receive_perform_later?(node)
return unless (runner_node = find_runner_node(node))

expect_node = runner_node.receiver
return unless expect_or_allow?(expect_node)
return if allow_receive_combination?(expect_node, node)

job_class = expect_node.first_argument
offense_node = find_offense_range(runner_node)
add_offense(offense_node,
message: offense_message(expect_node, job_class,
runner_node, node))
end

private

def allow_receive_combination?(expect_node, matcher_node)
expect_node.method?(:allow) && matcher_node.method?(:receive)
end

def offense_message(expect_node, job_class, runner_node, matcher_node)
format(MSG,
receiver: expect_node.method_name,
job_class: job_class.source,
to: runner_node.method_name,
matcher: matcher_node.method_name)
end

def find_runner_node(node)
node.each_ancestor(:send).find { |ancestor| runner?(ancestor) }
end

def find_offense_range(runner_node)
current = runner_node
current = current.parent while chained_send?(current)
current
end

def chained_send?(node)
node.parent&.send_type? && node.parent.receiver == node
end

def runner?(node)
RUNNERS.include?(node.method_name)
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/rubocop/cop/rspec_rails_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
require_relative 'rspec_rails/inferred_spec_type'
require_relative 'rspec_rails/minitest_assertions'
require_relative 'rspec_rails/negation_be_valid'
require_relative 'rspec_rails/receive_perform_later'
require_relative 'rspec_rails/travel_around'
Loading