使用 RSpec 测试 Retry RestClient

发布于 2025-01-15 04:51:15 字数 3060 浏览 2 评论 0原文

我正在使用 Oauth,所以我所做的就是在 User 表中存储 access_tokenrefresh token,我创建了一些类来执行此操作。在 Create 类中,我执行代码的正常功能(在集成上创建记录)。 access_token 将于 1 小时后过期,因此我决定执行 Refresh.new(user).call,而不是安排一个活动作业来刷新该令牌。 请求新的 access_tokenrefresh_token

我知道该代码有效,因为我已经进行了现场测试,并且当 access_token 过期时我得到了新的令牌。但我想为此做一个 rspec 测试。

我的rspec 测试的一部分:。

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

这是响应:

(RestClient).post(# data)
           expected: 1 time with any arguments
           received: 2 times with arguments: (# data)

这是我的代码:

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

我也尝试使用类似的东西这是来自这里

 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)

但我不知道如何正确处理。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

谁的年少不轻狂 2025-01-22 04:51:15

您的测试调用 RestClient.post 两次,首先在 Create 中调用,然后在 Retry 中再次调用。但你只嘲笑了一个电话。您需要模拟这两个调用。第一个调用引发异常,第二个调用返回成功结果。

我们可以通过 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

但是,这对代码的工作原理做出了很多假设,并且没有其他任何东西调用 RestClient.post

更强大的方法是使用 with 来指定具有特定参数的响应,并验证传递的参数是否正确。

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

但这仍然对代码的工作原理做出了很多假设,您需要做出适当的响应。

更好的方法是专注于您正在测试的内容:当创建调用收到未经授权的异常时,它会尝试刷新并再次执行调用。此单元测试不必还测试 Refresh#call 是否有效,只需测试 Create#call 调用它即可。您不需要让 RestClient.post 引发异常,只需 Create#create_contact 就可以。

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

并且还可以测试重试失败的时间。这些可以组合在一起。

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

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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文