Search code examples
rubyrspecjruby

Ruby Rspec class_double that returns new REST response each call


I have a Rspec test which needs to double the Request and HTTPI class/module and return a mocked REST response. I have this working, until this method makes another REST call and needs to return a new REST response.

API Class
NOTE this is a trimmed down version of the class but the gist is there

class API
  include HTTPI
  
  def find_device(id)
    req = create_request('path')
    req.body = { :customerId => MultiJson.dump(customer_id) }
    return call(req)
  end
  
  def find_other_device(other_id)
    req = create_request('path')
    req.body = { :other_id => MultiJson.dump(other_id) }
    data = call(req)
    return data
  end
  
  def call(req)
    response = HTTPI.send(req)
    return response.body
  end
end

Device file calling REST method

class Device
  @api = API.new(:open_timeout => 30, :read_timeout => 30)
  def get_device
    devices = @api.find_device(@id)
    log.info("First Call Made")
    other_call = @api.find_other_device(@other_id)
  end
end

spec file

Rspec.describe Device do
  resp = {code: 200, body: resp_body, raw_body: "TEST BODY", cookies: [cookie_list_hash]}
  resp2 = {code: 200, body: resp_body_2, raw_body: "TEST BODY 2", cookies: [cookie_list_hash]}
  let!(:request) {class_double('Request', new: http).as_stubbed_const} # I understand this causes the HTTPI send request to always return the same resp, but without it the test does not even get past the first call
  let!(:http) {class_double('HTTPI', send: resp).as_stubbed_const}

  it 'test' do
    Device.get_device
  end
end

The hope is to make a double that returns the resp var first and on the second call the :send, it returns resp2.

I am rather new to ruby also, so this may be pretty ugly.


Solution

  • I will focus on your spec, though there are some other things in your other classes that may need review (depending on what you want to achieve). Maybe if you write it another way you can get the logic behind it. First of all, you need to define the responses as lets as well; also, you can take a look at returning different values across multiple calls.

    Rspec.describe Device do
      let(:resp) do 
        {
          code: 200, body: resp_body, raw_body: "TEST BODY", cookies: [cookie_list_hash]
        }
      end
      let(:resp2) do
        {
          code: 200, body: resp_body_2, raw_body: "TEST BODY 2", cookies: [cookie_list_hash]
        }
      end
      let!(:request) { class_double('Request', new: http).as_stubbed_const }
      let!(:http) { class_double('HTTPI').as_stubbed_const }
    
      before do
        # see https://www.rubydoc.info/github/rspec/rspec-mocks/RSpec%2FMocks%2FMessageExpectation:and_return
        allow(http).to receive(:send).and_return(resp, resp2)
      end
    
      it 'test' do
        Device.get_device
      end
    end
    

    Having said that, which may resolve the problem with your spec, it seems that you also may want your api object to be an instance variable, and not something defined in your class:

    class Device
      def api
        # This will create the API object only once, and return it each time you call it in #get_device
        @api ||= API.new(:open_timeout => 30, :read_timeout => 30)
      end
    
      def get_device
        devices = api.find_device(@id)
        log.info("First Call Made")
        other_call = api.find_other_device(@other_id)
      end
    end
    

    But, again, this depends on what you want to achieve and if the code you pasted is complete/correct or not, so sorry if this doesn't apply.