Cameron
Cameron

Reputation: 28783

Using RSpec to test a retry block in a rescue

I have an instance method in a class like so:

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

and the try_request method contains a retry when the code is rescued, so that the block can be re-requested after doing something to rectify the Unauthorized exception:

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

So I'm trying to test this retry happens with the following test:

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

Which passes and gives me full code coverage of that rescue.

However what is actually happening is that because the request is raising the error, we end at this point straight away: EMS::Response.new(unauthorized_response) and because I'm NOT doing anything with the expects at this point in the test it's then continuing with the test (so considers all of that rescue code called) and then we are re-mocking out the response and expects so we end up with a test that thinks the entire block is fully tested code... but we don't actually verify that if the request is made... it successfully calls the same method again if the exception has been raised the first time, and THEN responds successfully or fails if the retry happens more than once.

How can I properly test these three scenarios?

1.) Test that the request retries once more if exception is raised.

2.) Test that if the second attempt passes I get a successful response.

3.) Test that if the second attempt fails, then I get that final EMS::Response.new(unauthorized_response) response.

Hopefully that makes sense? I looked into using should_receive(:retry) but I couldn't see how I would use this to verify that the actual same code is being re-called and how I could verify it only happens once.

Upvotes: 0

Views: 1973

Answers (1)

Greg
Greg

Reputation: 6628

I'd try ordered

You should be able to do this

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)

Next example

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)

And lastly

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

(You can use allow instead of expect if you want, I just think expect is more fitting)

If that doesn't work (I could not test it right away), you can always use the block version of double expectations (but this will be a bit more ugly)

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)

Upvotes: 2

Related Questions