'Using RSpec to test a Retry RestClient
I'm using Oauth so what I do is store access_token and refresh token at User table, I create some classes to do this. In the Create class I do the normal functionality of the code (create records on the integration). The access_token expire at 1 hour, so intead of schedule an active job to refresh that token at that time I decided to do Refresh.new(user).call to request a new access_token and refresh_token.
I know that code works, because I've tested on live and I'm getting the new token when the access_token is expired. But I want to do a rspec test for this.
part of my rspec test:.
context 'when token is expired' do
it 'request a refresh token and retry' do
old_key = user.access_token
allow(RestClient)
.to receive(:post)
.and_raise(RestClient::Unauthorized).once
expect { Create.new.call }.to change { user.reload.access_token }.from(old_key)
end
end
This is the response:
(RestClient).post(# data)
expected: 1 time with any arguments
received: 2 times with arguments: (# data)
This is my code:
Create.rb
class Create
def initialize(user)
@user = user
@refresh_token = user&.refresh_token
@access_token = user&.access_token
@logger = Rails.logger
@message = Crm::Message.new(self.class, 'User', user&.id)
end
def call
# validations
create_contact
rescue RestClient::Unauthorized => ex
retry if Refresh.new(user).call
rescue RestClient::ExceptionWithResponse => ex
logger.error(@message.api_error(ex))
raise
end
private
attr_reader :user, :logger, :access_token, :refresh_token
def create_contact
response = RestClient.post(
url, contact_params, contact_headers
)
logger.info(@message.api_response(response))
end
end
Refresh.rb
class Refresh
def initialize(user)
@user = user
@refresh_token = user&.refresh_token
@access_token = user&.access_token
@logger = Rails.logger
@message = Crm::Message.new(self.class, 'User', user&.id)
end
def call
# validations
refresh_authorization_code
end
def refresh_authorization_code
response = RestClient.post(url, authorization_params)
logger.info(@message.api_response(response))
handle_response(response)
end
private
attr_reader :user, :logger, :access_token, :refresh_token
def handle_response(response)
parsed = JSON.parse(response)
user.update!(access_token: parsed[:access_token], refresh_token: parsed[:refresh_token])
end
end
Also I tried using something like this from here
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)
but I don't know how handle it propertly.
Solution 1:[1]
Your test calls RestClient.post twice, first in Create then again in Retry. But you only mocked one call. You need to mock both calls. The first call raises an exception, the second responds with a successful result.
We could do this by specifying an order with ordered...
context 'when token is expired' do
it 'request a refresh token and retry' do
old_key = user.access_token
# First call fails
allow(RestClient)
.to receive(:post)
.and_raise(RestClient::Unauthorized)
.ordered
# Second call succeeds and returns an auth response.
# You need to write up that auth_response.
# Alternatively you can .and_call_original but you probably
# don't want your tests making actual API calls.
allow(RestClient)
.to receive(:post)
.and_return(auth_response)
.ordered
expect { Create.new.call }.to change { user.reload.access_token }.from(old_key)
end
end
However, this makes a lot of assumptions about exactly how the code works, and that nothing else calls RestClient.post.
More robust would be to use with to specify responses with specific arguments, and also verify the correct arguments are being passed.
context 'when token is expired' do
it 'request a refresh token and retry' do
old_key = user.access_token
# First call fails
allow(RestClient)
.to receive(:post)
.with(...whatever the arguments are...)
.and_raise(RestClient::Unauthorized)
# Second call succeeds and returns an auth response.
# You need to write up that auth_response.
# Alternatively you can .and_call_original but you probably
# don't want your tests making actual API calls.
allow(RestClient)
.to receive(:post)
.with(...whatever the arguments are...)
.and_return(auth_response)
expect { Create.new.call }.to change { user.reload.access_token }.from(old_key)
end
end
But this still makes a lot of assumptions about exactly how the code works, and you need to make a proper response.
Better would be to focus in on exactly what you're testing: when the create call gets an unauthorized exception it tries to refresh and does the call again. This unit test doesn't have to also test that Refresh#call works, just that Create#call calls it. You don't need to have RestClient.post raise an exception, just that Create#create_contact does.
context 'when token is expired' do
it 'requests a refresh token and retry' do
old_key = user.access_token
create = Create.new(user)
# First call fails
allow(create)
.to receive(:create_contact)
.and_raise(RestClient::Unauthorized)
.ordered
# It refreshes
refresh = double
expect(Refresh)
.to receive(:new)
.with(user)
.and_return(refresh)
# The refresh succeeds
expect(refresh)
.to receive(:call)
.with(no_args)
.and_return(true)
# It tries again
expect(create)
.to receive(:create_contact)
.ordered
create.call
end
end
And you can also test when the retry fails. These can be combined together.
context 'when token is expired' do
let(:refresh) { double }
let(:create) { Create.new(user) }
before {
# First call fails
allow(create)
.to receive(:create_contact)
.and_raise(RestClient::Unauthorized)
.ordered
# It tries to refresh
expect(Refresh)
.to receive(:new)
.with(user)
.and_return(refresh)
}
context 'when the refresh succeeds' do
before {
# The refresh succeeds
allow(refresh)
.to receive(:call)
.with(no_args)
.and_return(true)
}
it 'retries' do
expect(create)
.to receive(:create_contact)
.ordered
create.call
end
end
context 'when the refresh fails' do
before {
# The refresh succeeds
allow(refresh)
.to receive(:call)
.with(no_args)
.and_return(false)
}
it 'does not retry' do
expect(create)
.not_to receive(:create_contact)
.ordered
create.call
end
end
end
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Schwern |
