【问题标题】:How to properly test ActiveJob's retry_on method with rspec?如何使用 rspec 正确测试 ActiveJob 的 retry_on 方法?
【发布时间】:2023-03-18 18:56:01
【问题描述】:

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

我想做的另一件事是rescue 在进行最后一次重试后冒泡的错误。

请看下面我的 cmets 和代码 sn-ps。

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 的类方法,我已经在byebug 终端中确认了我的工作类就是这种情况。

这个测试不应该工作吗?它期望类接收带有某些参数的类方法。当我将 byebug 放入 retry_on 块时,我的 byebug 也会被命中,因此我知道该方法被多次调用。

这几乎就像是在另一个班级被调用,这非常令人困惑,我不认为是这种情况,但我已经走到了尽头。

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

但是,这适用于多个测试用例。如果您在多个案例中使用它,最后一个测试将中断,并说它执行的工作比预期的要多。

 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

对此的任何帮助/见解将不胜感激。

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

感谢精彩的 Stack Overflow 社区!

【问题讨论】:

  • 您似乎正在尝试测试attempts 参数是否正常工作,对其进行代码和测试已经是rails框架的一部分。您无需测试它是您的域逻辑的重要组成部分

标签: ruby-on-rails rspec rspec-rails ruby-on-rails-5.1 rails-activejob


【解决方案1】:

虽然我同意@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

【讨论】:

    【解决方案2】:

    恕我直言,您应该将 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
    

    【讨论】:

    • load 'app/path/to/job/my_job.rb' 应该符合对retry_on 的期望。
    【解决方案3】:

    以下对我来说很好,也适用于多个测试用例和测试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 不算作“入队作业”。因此,described_class.perform_now 会导致 assert_performed_jobs 计数的尝试次数减少。

    【讨论】:

      【解决方案4】:

      这是 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 时都会运行、设置和检查。我们模拟它返回 10,然后期望它调用 Bugsnag。 retry_on 仅在满足所有 attempts 后才调用您在块中提供的内容。所以这行得通。

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

      【讨论】:

      • 在 Rails 6.1 上必须存根 executions_for 而不是 executions
      【解决方案5】:

      在第一个规范中

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

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

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

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

      assert_performed_jobs 3
      

      不通过障碍将无法工作

      类似

      assert_performed_jobs 2 do
        //call jobs from here
      end
      

      【讨论】:

      • 感谢您的回复!我确实按照您的建议尝试了上面的块,但是我仍然遇到了问题,我希望运行 10 个作业,它会运行 23 个。我还在钩子之前和之后设置了尝试在两种情况之间清除它们,但是它没有一个区别。它继续运行 20 多个工作。如果我只有一个测试用例,则测试通过,但是当我添加多个测试用例时,它们开始失败,因为测试用例之间的作业似乎没有被清除。
      猜你喜欢
      • 2021-09-26
      • 2019-11-24
      • 1970-01-01
      • 2023-03-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-04-20
      相关资源
      最近更新 更多