【问题标题】:Using RSpec to test a retry block in a rescue在救援中使用 RSpec 测试重试块
【发布时间】:2025-12-04 12:00:01
【问题描述】:

我在这样的类中有一个实例方法:

def get(path)
  try_request do
    RestClient.get(path, headers)
  end
end

try_request方法在代码被抢救时包含了retry,这样在对Unauthorized异常做一些处理后可以重新请求block:

def try_request
  tries ||= 2
  EMS::Response.new(yield)
rescue RestClient::Unauthorized => e
  tries -= 1
  if tries.positive?
    ems_credentials.update_and_return_token
    retry
  end
  Rails.logger.error "Exception on #{e.class}: #{e.message}"
  EMS::Response.new(unauthorized_response)
end

所以我正在尝试通过以下测试来测试此重试:

it 'retries and returns a successful response' do
  # allow the RestClient to raise an Unauthorized response
  allow(RestClient).to receive(:get).and_raise(RestClient::Unauthorized)
  # call the endpoint which would cause the above error
  described_class.new.get('/endpoint')
  # mock out a successful response:
  rest_response = double('rest_response', code: 200, body: { test: 'Test' }.to_json)
  # update the RestClient to return the successful response this time
  allow(RestClient).to receive(:get).and_return(rest_response)
  # retry the request and check the response is as expected:
  client_response = described_class.new.get('/endpoint')
  expect(client_response.code).to eq(200)
  expect(client_response.success?).to be_truthy
  expect(client_response.body[:test]).to eq('Test')
  expect(client_response).to be_an_instance_of(EMS::Response)
end

这通过并为我提供了该救援的完整代码覆盖率。

但是实际发生的情况是,由于请求引发了错误,我们立即在这一点结束:EMS::Response.new(unauthorized_response) 并且因为在测试的这一点上我没有对 expects 做任何事情然后继续测试(所以考虑所有调用的救援代码),然后我们重新模拟响应并期望,所以我们最终得到一个认为整个块是完全测试代码的测试......但我们没有' t 实际上验证是否发出请求...如果第一次引发异常,它会再次成功调用相同的方法,如果重试发生多次,则 THEN 成功响应或失败。

如何正确测试这三种场景?

1.) 测试如果引发异常,请求会再次重试。

2.) 测试如果第二次尝试通过,我得到一个成功的响应。

3.) 测试如果第二次尝试失败,那么我会得到最终的EMS::Response.new(unauthorized_response) 响应。

希望这是有道理的?我研究过使用should_receive(:retry),但我不知道如何使用它来验证实际相同的代码是否被重新调用,以及我如何验证它只发生一次。

【问题讨论】:

标签: ruby-on-rails ruby rspec


【解决方案1】:

我会试试ordered

你应该可以做到的

expect(RestClient).to receive(:get).ordered.and_raise(RestClient::Unauthorized)
expect(RestClient).to receive(:get).ordered.and_return(rest_response)
# ...
expect(client_response.code).to eq(200)

下一个例子

expect(RestClient).to receive(:get).ordered.and_raise(RestClient::Unauthorized)
expect(RestClient).to receive(:get).ordered.and_raise(RestClient::Unauthorized)
expect(RestClient).to receive(:get).ordered.and_return(rest_response)
# ...
expect(client_response.code).to eq(200)

最后

expect(RestClient).to receive(:get).ordered.and_raise(RestClient::Unauthorized)
# check that it was unsuccesfull

(如果你愿意,你可以用allow代替expect,我只是觉得expect更合适)

如果这不起作用(我无法立即对其进行测试),您始终可以使用双重期望的block version(但这会更难看)

errors_to_raise = 2
allow(RestClient).to receive(:get) do
  return rest_response if errors_to_raise <= 0
  errors_to_raise -= 1
  raise RestClient::Unauthorized
end
# ...
expect(client_response.code).to eq(200)

【讨论】: