From 377cb75a82931436a498cda232cddc911d376135 Mon Sep 17 00:00:00 2001 From: ydah Date: Sat, 15 Nov 2025 16:53:50 +0900 Subject: [PATCH] Add new `RSpecRails/ReceivePerformLater` cop Fixes: https://github.com/rubocop/rubocop-rspec_rails/issues/72 --- CHANGELOG.md | 2 + config/default.yml | 6 + docs/modules/ROOT/pages/cops.adoc | 1 + docs/modules/ROOT/pages/cops_rspecrails.adoc | 54 +++ .../cop/rspec_rails/receive_perform_later.rb | 105 ++++ lib/rubocop/cop/rspec_rails_cops.rb | 1 + .../rspec_rails/receive_perform_later_spec.rb | 450 ++++++++++++++++++ 7 files changed, 619 insertions(+) create mode 100644 lib/rubocop/cop/rspec_rails/receive_perform_later.rb create mode 100644 spec/rubocop/cop/rspec_rails/receive_perform_later_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index b2d6365..5a2f6f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Master (Unreleased) +- Add new `RSpecRails/ReceivePerformLater` cop. ([@ydah]) + ## 2.32.0 (2025-11-12) - Add `RSpecRails/HttpStatusNameConsistency` cop. ([@taketo1113]) diff --git a/config/default.yml b/config/default.yml index a8c35fb..d9ee278 100644 --- a/config/default.yml +++ b/config/default.yml @@ -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 diff --git a/docs/modules/ROOT/pages/cops.adoc b/docs/modules/ROOT/pages/cops.adoc index 489ea9e..a1d7b78 100644 --- a/docs/modules/ROOT/pages/cops.adoc +++ b/docs/modules/ROOT/pages/cops.adoc @@ -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 diff --git a/docs/modules/ROOT/pages/cops_rspecrails.adoc b/docs/modules/ROOT/pages/cops_rspecrails.adoc index 6595465..9eced29 100644 --- a/docs/modules/ROOT/pages/cops_rspecrails.adoc +++ b/docs/modules/ROOT/pages/cops_rspecrails.adoc @@ -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 diff --git a/lib/rubocop/cop/rspec_rails/receive_perform_later.rb b/lib/rubocop/cop/rspec_rails/receive_perform_later.rb new file mode 100644 index 0000000..08db6e3 --- /dev/null +++ b/lib/rubocop/cop/rspec_rails/receive_perform_later.rb @@ -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(%s)` ' \ + 'over `%s(%s).%s ' \ + '%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 diff --git a/lib/rubocop/cop/rspec_rails_cops.rb b/lib/rubocop/cop/rspec_rails_cops.rb index 59d200b..e48cddc 100644 --- a/lib/rubocop/cop/rspec_rails_cops.rb +++ b/lib/rubocop/cop/rspec_rails_cops.rb @@ -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' diff --git a/spec/rubocop/cop/rspec_rails/receive_perform_later_spec.rb b/spec/rubocop/cop/rspec_rails/receive_perform_later_spec.rb new file mode 100644 index 0000000..c336ec1 --- /dev/null +++ b/spec/rubocop/cop/rspec_rails/receive_perform_later_spec.rb @@ -0,0 +1,450 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::RSpecRails::ReceivePerformLater do + it 'registers an offense when using ' \ + '`expect(Job).to receive(:perform_later)`' do + expect_offense(<<~RUBY) + it 'enqueues a job' do + expect(MyJob).to receive(:perform_later) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to receive(:perform_later)`. + do_something + end + RUBY + end + + it 'registers an offense when using ' \ + '`expect(Job).not_to receive(:perform_later)`' do + expect_offense(<<~RUBY) + it 'does not enqueue a job' do + expect(MyJob).not_to receive(:perform_later) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).not_to receive(:perform_later)`. + do_something + end + RUBY + end + + it 'registers an offense when using ' \ + '`expect(Job).to_not receive(:perform_later)`' do + expect_offense(<<~RUBY) + it 'does not enqueue a job' do + expect(MyJob).to_not receive(:perform_later) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to_not receive(:perform_later)`. + do_something + end + RUBY + end + + it 'registers an offense when using ' \ + '`expect(Job).to receive(:perform_later).with(...)`' do + expect_offense(<<~RUBY) + it 'enqueues a job with arguments' do + expect(MyJob).to receive(:perform_later).with(user, order) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to receive(:perform_later)`. + do_something + end + RUBY + end + + it 'registers an offense when using ' \ + '`expect(Job).to receive(:perform_later).with(keyword:)`' do + expect_offense(<<~RUBY) + it 'enqueues a job with keyword arguments' do + expect(MyJob).to receive(:perform_later).with(user_id: 1) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to receive(:perform_later)`. + do_something + end + RUBY + end + + it 'registers an offense when using ' \ + '`expect(Job).to receive(:perform_later).with(...).and_return(...)`' do + expect_offense(<<~RUBY) + it 'enqueues a job with chained methods' do + expect(MyJob).to receive(:perform_later).with(user).and_return(true) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to receive(:perform_later)`. + do_something + end + RUBY + end + + it 'registers an offense when using ' \ + '`expect(Job).to have_received(:perform_later)`' do + expect_offense(<<~RUBY) + it 'enqueues a job' do + allow(MyJob).to receive(:perform_later) + do_something + expect(MyJob).to have_received(:perform_later) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to have_received(:perform_later)`. + end + RUBY + end + + it 'registers an offense when using ' \ + '`expect(Job).to have_received(:perform_later).with(...)`' do + expect_offense(<<~RUBY) + it 'enqueues a job with arguments' do + allow(MyJob).to receive(:perform_later) + do_something + expect(MyJob).to have_received(:perform_later).with(user) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to have_received(:perform_later)`. + end + RUBY + end + + it 'registers an offense when using ' \ + '`expect(Job).not_to have_received(:perform_later)`' do + expect_offense(<<~RUBY) + it 'does not enqueue a job' do + allow(MyJob).to receive(:perform_later) + do_something + expect(MyJob).not_to have_received(:perform_later) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).not_to have_received(:perform_later)`. + end + RUBY + end + + it 'registers an offense when using ' \ + '`expect(Job).to_not have_received(:perform_later)`' do + expect_offense(<<~RUBY) + it 'does not enqueue a job' do + allow(MyJob).to receive(:perform_later) + do_something + expect(MyJob).to_not have_received(:perform_later) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to_not have_received(:perform_later)`. + end + RUBY + end + + it 'does not register an offense when using ' \ + '`allow(Job).to receive(:perform_later)`' do + expect_no_offenses(<<~RUBY) + it 'allows job enqueuing' do + allow(MyJob).to receive(:perform_later) + do_something + end + RUBY + end + + it 'does not register an offense when using ' \ + '`allow(Job).to receive(:perform_later).with(...)`' do + expect_no_offenses(<<~RUBY) + it 'allows job enqueuing with args' do + allow(MyJob).to receive(:perform_later).with(user) + do_something + end + RUBY + end + + it 'does not register an offense when using ' \ + '`allow(Job).to receive(:perform_later).and_return(...)`' do + expect_no_offenses(<<~RUBY) + it 'allows job enqueuing with return value' do + allow(MyJob).to receive(:perform_later).and_return(job_instance) + do_something + end + RUBY + end + + it 'does not register an offense when using ' \ + '`expect { ... }.to have_enqueued_job(Job)`' do + expect_no_offenses(<<~RUBY) + it 'enqueues a job' do + expect { do_something }.to have_enqueued_job(MyJob) + end + RUBY + end + + it 'does not register an offense when using ' \ + '`expect { ... }.to have_enqueued_job(Job).with(...)`' do + expect_no_offenses(<<~RUBY) + it 'enqueues a job with arguments' do + expect { do_something }.to have_enqueued_job(MyJob).with(user, order) + end + RUBY + end + + it 'does not register an offense when using ' \ + '`expect { ... }.to have_enqueued_job(Job)` with chained matchers' do + expect_no_offenses(<<~RUBY) + it 'enqueues a job with options' do + expect { do_something } + .to have_enqueued_job(MyJob) + .on_queue('mailers') + .at(Date.tomorrow.noon) + end + RUBY + end + + it 'does not register an offense when using ' \ + '`expect { ... }.not_to have_enqueued_job(Job)`' do + expect_no_offenses(<<~RUBY) + it 'does not enqueue a job' do + expect { do_something }.not_to have_enqueued_job(MyJob) + end + RUBY + end + + it 'does not register an offense when using ' \ + '`expect { ... }.to_not have_enqueued_job(Job)`' do + expect_no_offenses(<<~RUBY) + it 'does not enqueue a job' do + expect { do_something }.to_not have_enqueued_job(MyJob) + end + RUBY + end + + it 'does not register an offense when using ' \ + '`expect(Job).to receive(:perform_now)`' do + expect_no_offenses(<<~RUBY) + it 'performs a job' do + expect(MyJob).to receive(:perform_now) + do_something + end + RUBY + end + + it 'does not register an offense when using ' \ + '`expect(Job).to receive(:other_method)`' do + expect_no_offenses(<<~RUBY) + it 'calls some method' do + expect(MyJob).to receive(:some_method) + do_something + end + RUBY + end + + it 'does not register an offense when using ' \ + '`expect(Job).to have_received(:other_method)`' do + expect_no_offenses(<<~RUBY) + it 'has received some method' do + allow(MyJob).to receive(:other_method) + do_something + expect(MyJob).to have_received(:other_method) + end + RUBY + end + + it 'does not register an offense when using ' \ + '`expect(instance).to receive(:perform_later)`' do + expect_no_offenses(<<~RUBY) + it 'receives a method' do + expect(instance).to receive(:perform_later) + instance.perform_later + end + RUBY + end + + it 'does not register an offense when using ' \ + '`expect(variable).to receive(:perform_later)`' do + expect_no_offenses(<<~RUBY) + it 'receives a method on variable' do + job = MyJob.new + expect(job).to receive(:perform_later) + job.perform_later + end + RUBY + end + + it 'registers an offense for namespaced job class' do + expect_offense(<<~RUBY) + it 'enqueues a namespaced job' do + expect(Jobs::MyJob).to receive(:perform_later) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(Jobs::MyJob)` over `expect(Jobs::MyJob).to receive(:perform_later)`. + do_something + end + RUBY + end + + it 'registers an offense for deeply nested job class' do + expect_offense(<<~RUBY) + it 'enqueues a deeply nested job' do + expect(Company::Jobs::MyJob).to receive(:perform_later) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(Company::Jobs::MyJob)` over `expect(Company::Jobs::MyJob).to receive(:perform_later)`. + do_something + end + RUBY + end + + it 'does not register an offense when parent is not a send node' do + expect_no_offenses(<<~RUBY) + it 'handles non-send parent' do + receive(:perform_later) + end + RUBY + end + + it 'does not register an offense when to_node receiver is nil' do + expect_no_offenses(<<~RUBY) + it 'handles missing receiver' do + to receive(:perform_later) + end + RUBY + end + + it 'does not register an offense when expect has no arguments' do + expect_no_offenses(<<~RUBY) + it 'handles expect without args' do + expect().to receive(:perform_later) + end + RUBY + end + + it 'does not register an offense when job_class is not a constant' do + expect_no_offenses(<<~RUBY) + it 'handles non-constant job class' do + job_class = MyJob + expect(job_class).to receive(:perform_later) + end + RUBY + end + + it 'registers an offense with expect and to' do + expect_offense(<<~RUBY) + it 'uses expect and to' do + expect(MyJob).to receive(:perform_later) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to receive(:perform_later)`. + end + RUBY + end + + it 'registers an offense with expect and not_to' do + expect_offense(<<~RUBY) + it 'uses expect and not_to' do + expect(MyJob).not_to receive(:perform_later) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).not_to receive(:perform_later)`. + end + RUBY + end + + it 'registers an offense with expect and to_not' do + expect_offense(<<~RUBY) + it 'uses expect and to_not' do + expect(MyJob).to_not receive(:perform_later) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to_not receive(:perform_later)`. + end + RUBY + end + + it 'registers an offense with allow and have_received' do + expect_offense(<<~RUBY) + it 'uses allow and have_received' do + allow(MyJob).to receive(:perform_later) + MyJob.perform_later + expect(MyJob).to have_received(:perform_later) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to have_received(:perform_later)`. + end + RUBY + end + + it 'does not register an offense with allow and receive (no expect)' do + expect_no_offenses(<<~RUBY) + it 'uses allow and receive only' do + allow(MyJob).to receive(:perform_later) + MyJob.perform_later + end + RUBY + end + + it 'registers offenses for multiple jobs' do + expect_offense(<<~RUBY) + it 'enqueues multiple jobs' do + expect(FirstJob).to receive(:perform_later) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(FirstJob)` over `expect(FirstJob).to receive(:perform_later)`. + expect(SecondJob).to receive(:perform_later) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(SecondJob)` over `expect(SecondJob).to receive(:perform_later)`. + do_something + end + RUBY + end + + it 'does not register an offense ' \ + 'when no runner node is found' do + expect_no_offenses(<<~RUBY) + it 'handles case where no runner is found' do + SomeClass.method_chain.receive(:perform_later) + end + RUBY + end + + it 'does not register an offense ' \ + 'when parent becomes nil in search' do + expect_no_offenses(<<~RUBY) + it 'handles nil parent case' do + receive(:perform_later) + end + RUBY + end + + it 'registers offense for simple case ' \ + 'without method chaining' do + expect_offense(<<~RUBY) + it 'simple case' do + expect(MyJob).to receive(:perform_later) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to receive(:perform_later)`. + end + RUBY + end + + it 'registers offense when parent is not send_type' do + expect_offense(<<~RUBY) + it 'parent not send type' do + result = (expect(MyJob).to receive(:perform_later)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to receive(:perform_later)`. + end + RUBY + end + + it 'registers offense with multi-level method chaining' do + expect_offense(<<~RUBY) + it 'multi level chaining' do + expect(MyJob).to receive(:perform_later).with(arg1).with(arg2).and_return(value) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to receive(:perform_later)`. + end + RUBY + end + + it 'registers offense when assigned to variable' do + expect_offense(<<~RUBY) + it 'assignment context' do + expectation = expect(MyJob).to receive(:perform_later) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to receive(:perform_later)`. + end + RUBY + end + + it 'registers offense when nested in method call' do + expect_offense(<<~RUBY) + it 'tests parent.receiver != current early exit' do + other_method(expect(MyJob).to receive(:perform_later)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to receive(:perform_later)`. + end + RUBY + end + + it 'registers offense in begin block with semicolon' do + expect_offense(<<~RUBY) + it 'parent not send type in find_offense_node' do + result = (expect(MyJob).to receive(:perform_later); other_code) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to receive(:perform_later)`. + end + RUBY + end + + it 'registers offense with chaining after expectation' do + expect_offense(<<~RUBY) + it 'chaining after to' do + result = expect(MyJob).to(receive(:perform_later)).tap { |x| x } + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to receive(:perform_later)`. + end + RUBY + end + + it 'registers offense with deeply nested method chain' do + # This tests the case where find_offense_range traverses up + # until it reaches a node whose parent is nil + expect_offense(<<~RUBY) + expect(MyJob).to(receive(:perform_later)).foo.bar.baz + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `expect { ... }.to have_enqueued_job(MyJob)` over `expect(MyJob).to receive(:perform_later)`. + RUBY + end +end