如何使用 rspec 正确测试 ActiveJob 的 retry_on 方法?

How to properly test ActiveJob's retry_on method with rspec?

过去几天我一直在尝试测试这个方法,但没有成功。

我希望能够做的另一件事是 rescue 在最后一次重试尝试后出现的错误。

请在下面查看我的评论和代码片段。

Source code for retry_on is here as well for context.

下面是示例代码和测试:

   my_job.rb

   retry_on Exception, wait: 2.hours, attempts: 3 do |job, exception|
   # some kind of rescue here after job.exceptions == 3  
   # then notify Bugsnag of failed final attempt.
   end

   def perform(an_object)
     an_object.does_something
   end

   my_spec.rb
   it 'receives retry_on 3 times' do
     perform_enqueued_jobs do
       expect(AnObject).to receive(:does_something).and_raise { Exception }.exactly(3).times
       expect(MyJob).to receive(:retry_on).with(wait: 2.hours, attempts: 3).exactly(3).times
       MyJob.perform_later(an_object)
     end
     assert_performed_jobs 3
   end

测试失败响应:

      1) MyJob.perform receives retry_on 3 times
         Failure/Error: expect(job).to receive(:retry_on).with(wait: 4.hours, attempts: 3).exactly(3).times

   (MyJob (class)).retry_on({:wait=>2 hours, :attempts=>3})
       expected: 3 times with arguments: ({:wait=>2 hours, :attempts=>3})
       received: 0 times
 # ./spec/jobs/my_job_spec.rb:38:in `block (4 levels) in <top (required)>'
 # ./spec/rails_helper.rb:48:in `block (3 levels) in <top (required)>'
 # ./spec/rails_helper.rb:47:in `block (2 levels) in <top (required)>'

我也试过把这个工作做成一个双倍的工作并打桩 retry_on 方法,但这也不起作用。

我也尝试过使用 Timecop 来快进等待时间,但测试仍然失败:

           my_spec.rb
   it 'receives retry_on 3 times' do
     perform_enqueued_jobs do
       expect(AnObject).to receive(:does_something).and_raise { Exception }.exactly(3).times
       Timecop.freeze(Time.now + 8.hours) do
         expect(MyJob).to receive(:retry_on).with(wait: 2.hours, attempts: 3).exactly(3).times
       end
       MyJob.perform_later(an_object)
     end
     assert_performed_jobs 3
   end

它是 ActiveJob 的 class 方法,我已经在 byebug 终端中确认了我的工作 class 的情况。

这个测试应该行不通吗?它期望 class 接收带有特定参数的 class 方法。当我将 byebug 放入 retry_on 块时,我的 byebug 也被命中,所以我知道该方法被多次调用。

这几乎就像是在另一个 class 上调用它,这非常令人困惑,我不认为是这种情况,但我已经无能为力了。

我几乎解决了这个问题,方法是将我的测试从测试 retry_on rails 逻辑本身解耦到围绕它测试我的业务逻辑。这种方式在 rails 曾经改变 retry_on 逻辑的情况下也更好。

但是,这 NOT 对不止一个测试用例有效。如果您将它用于多个案例,最后一个测试将中断并说它执行的工作比预期的多。

 my_spec.rb
 it 'receives retry_on 3 times' do
   perform_enqueued_jobs do
     allow(AnObject).to receive(:does_something).and_raise { Exception }
     expect(AnObject).to receive(:does_something).exactly(3).times
     expect(Bugsnag).to receive(:notify).with(Exception).once
     MyJob.perform_later(an_object)
   end
   assert_performed_jobs 3
 end

my_job.rb

retry_on Exception, wait: , attempts: 3 do |job, exception|
  Bugsnag.notify(exception)
end

def perform(an_object)
  an_object.does_something
end

任何help/insight对此将不胜感激。

也希望获得有关如何在最大尝试后处理冒泡异常的建议。我正在考虑在 retry_on 块中引发错误,然后让 discard_on 触发引发的错误。

感谢精彩的 Stack Overflow 社区!

这是 retry_on 所需的规范格式,最终对我有用:

it 'receives retry_on 10 times' do
  allow_any_instance_of(MyJob).to receive(:perform).and_raise(MyError.new(nil))
  allow_any_instance_of(MyJob).to receive(:executions).and_return(10)
  expect(Bugsnag).to receive(:notify)
  MyJob.perform_now(an_object)
end

it 'handles error' do
  allow_any_instance_of(MyJob).to receive(:perform).and_raise(MyError.new(nil))
  expect_any_instance_of(MyJob).to receive(:retry_job)
  perform_enqueued_jobs do
    MyJob.perform_later(an_object)
  end
end

对于第一种情况, executions 是一个 ActiveJob 方法,每次执行 retry_on 时都会获取 运行、设置和检查。我们将其模拟为 return 10,然后期望它调用 Bugsnag。 retry_on 仅在满足所有 attempts 后才调用您在块中提供的内容。所以这有效。

对于第二种情况, 然后模拟要为作业实例引发的错误。 接下来我们检查它是否正确接收 retry_jobretry_on 在幕后调用)以确认它正在做正确的事情。 然后我们将 perform_later 调用包装在 minitest perform_enqueued_jobs 块中并结束它。

在第一个规范中

expect(MyJob).to receive(:retry_on).with(wait: 2.hours, attempts:3).exactly(3).times

这永远不会起作用,因为 class 方法 retry_on 将在 class 初始化阶段调用,即在将 class 加载到内存时调用,而不是在执行规格

在第二个规范中,您尝试使用 timecop 使其工作,但由于同样的原因仍然失败

第三个规格相对更现实,但是

assert_performed_jobs 3

不通过方块就不行

类似

assert_performed_jobs 2 do
  //call jobs from here
end

以下对我来说效果很好,也适用于多个测试用例和测试 retry_on 块的副作用。

RSpec.describe MyJob, type: :job do
  include ActiveJob::TestHelper

  context 'when `MyError` is raised' do
    before do
      allow_any_instance_of(described_class).to receive(:perform).and_raise(MyError.new)
    end

    it 'makes 4 attempts' do
      assert_performed_jobs 4 do
        described_class.perform_later rescue nil
      end
    end

    it 'does something in the `retry_on` block' do
      expect(Something).to receive(:something)

      perform_enqueued_jobs do
        described_class.perform_later rescue nil
      end
    end
  end
end

请注意,如果您让异常在最后冒出来,则需要 rescue nil(或某种形式的救援)。

请注意 perform_now 不算作 "enqueued job"。因此,described_class.perform_now 会导致 assert_performed_jobs 的尝试次数减少一次。

恕我直言,您应该将 ActiveJob 的测试留给 rails 团队。

您只需确保正确配置作业:

it 'retries the job 10 times with 2 minutes intervals' do
  allow(MyJob).to receive(:retry_on)
  load 'app/path/to/job/my_job.rb'
  expect(MyJob).to have_received(:retry_on)
    .with(
      Exception,
      wait: 2.minutes,
      attempts: 10
    )
end

虽然我同意@fabriciofreitag 的观点,即不需要测试外部库的内部结构,但我认为确认您的 retry_on 块是否配置正确绝对有价值。此设置对我有用,无需担心 ActiveJob 如何管理重试。

# app/jobs/my_job.rb
retry_on TimeoutError,
         wait: :exponentially_longer,
         attempts: 3

# spec/jobs/my_job_spec.rb
describe "error handling" do
  before do
    ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true
  end

  context "when TimeoutError is raised" do
    it "retries timed-out requests" do
      expect(client).to receive(:connect).ordered.and_raise(TimeoutError)
      expect(client).to receive(:connect).ordered.and_call_original
      described_class.perform_now
    end

    it "retries three times before re-raising" do
      expect(client).to receive(:connect)
        .exactly(3).times.and_raise(TimeoutError)
      expect { described_class.perform_now }.to raise_error(TimeoutError)
    end
  end
end