如何使用 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_job
(retry_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
过去几天我一直在尝试测试这个方法,但没有成功。
我希望能够做的另一件事是 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_job
(retry_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