I have seldom used doubles, except when working with API requests. There are two main reasons why we might need to create doubles when working with APIS:

  • making the tests deterministic.
  • working with API call limit

I’ll go over both of these case, and give examples of how we can proceed.

Working with alternating responses from an API

The return we get from an API is often not the same when called twice, our tests therefore cannot pass when being run a second time.

To try all of this out you can find an appropriate API from this Twilio post.


require 'net/http'
require 'json'

class Affirmation

  def say_something
    response = make_request_to_api
    "I wanted to tell you: #{response["affirmation"]}"
  end

  def make_request_to_api
    url = 'https://www.affirmations.dev/'
    text_response = Net::HTTP.get(URI(url))
    JSON.parse(text_response)<1>
  end

end
  1. We can either use get or get_response; if using the latter, then .body needs to be called on the text_response.

The above class has two methods, one of which make the api call, the other’s functionality is to do something with the return of that call. The api we’re using returns random affirmations of the day, so trying to test it will result in tests only passing when the random response is the one we started the test with.

To write a test to check the method say_something we would first need to slightly modify the code above:

require 'net/http'
require 'json'

class Affirmation

  
  def initialize(connector)
    @connector = connector
  end

  def say_something
    response = make_request_to_api
    "I wanted to tell you: #{response["affirmation"]}"
  end

  def make_request_to_api
    url= 'https://www.affirmations.dev/'
    text_response = @connector.get(URI(url))
    JSON.parse(text_response)
  end

end

We replace Net:HTTP with connector that we allow to be passed in to the class, that way we are able to control what object is passed in and what we get out of it. Find more info on Net::HTTP here: http://www.rubyinside.com/nethttp-cheat-sheet-2940.html

Having done this, we can now control the return of a “double” tha we create inside the test:

require './affirmation'

RSpec.describe 'Affirmation' do

  it "calls an Api to provide an affirmation" do
    requester_dbl = double :requester

     expect(requester_dbl).to receive(:get)
     .with(URI("https://www.affirmations.dev/"))
     .and_return('{"affirmation": "Everything has cracks - that is how the light gets in"}')

    affirmation = Affirmation.new(requester_dbl)
    result = affirmation.say_something
    expect(result).to eq "I wanted to tell you: Everything has cracks - that is how the light gets in"
  end

end

In the above test, we still rely on the url being the same as inside our class, however. If what we expected the double to be given was different from the url inside the code it would fail; unless we remove it:

require './affirmation'

RSpec.describe 'Affirmation' do

  it "calls an Api to provide an affirmation" do
    requester_dbl = double :requester

     expect(requester_dbl).to receive(:get)
     .and_return('{"affirmation": "Everything has cracks - that is how the light gets in"}')

    affirmation = Affirmation.new(requester_dbl)
    result = affirmation.say_something
    expect(result).to eq "I wanted to tell you: Everything has cracks - that is how the light gets in"
  end

end

In this way, we are only mocking the response, and not what goes in. Below, I will go over another way to do this instead.

But first I just want to make sure that we can still use our class without having to specifiy to use Net::HTTP if something is not passed in somewhere in our program:

require 'net/http'
require 'json'

class Affirmation

  def initialize(connector = Net::HTTP)
    @connector = connector
  end

  def say_something
    response = make_request_to_api
    "I wanted to tell you: #{response["affirmation"]}"
  end

  def make_request_to_api
    url= 'https://www.affirmations.dev/'
    text_response = @connector.get(URI(url))
    JSON.parse(text_response)
  end

end

Now the default value of connector will always be Net::HTTP unless we specifiy what should go in instead.

Working with a limited call API

When working with an API that has certain restrictions, we may only have a limited call number for the day, and running our tests will mean that we are using some of those calls. We sometimes might only have 100. For a user, or for ourselves that might seem sufficient. But if we have multiple tests that all require a real API call, that number will quickly be reduced.

If we have other parameters to pass in to the class or a method on it however, the above may not be appropriate. Instead, we could write something like this instead:

class Affirmation

  
  def initialize(connector = Net::HTTP)
    @connector = connector
  end

  def say_something
    response = make_request_to_api
    "I wanted to tell you: #{response["affirmation"]}"
  end

  def make_request_to_api
    url= 'https://www.affirmations.dev/'
    text_response = @connector.get(URI(url))
    JSON.parse(text_response)
  end

  def get_key
    make_request_to_api[:key]
  end

end

require './affirmation'

RSpec.describe 'Affirmation' do

  let(:mock_response) {{key: 'I just want to check the value of the call response'}}

  it "calls an Api to provide an affirmation" do
    requester_dbl = double :requester

    affirmation = Affirmation.new
    allow(affirmation).to receive(:make_request_to_api).and_return(mock_response)
    result = affirmation.make_request_to_api
    expect(result.get_key).to eq "I just want to check the value of the call response"
  end
end

Here is another example of when we might need to use this:

# my_api.rb
class MyApi
  def initialize(api_key)
    @api_key = api_key
  end

  def call(input)
    # Make a real API call here
  end
end

# my_api_spec.rb
require 'rspec'
require_relative 'my_api'

RSpec.describe MyApi do
  let(:api_key) { 'test_api_key' }
  let(:input) { 'test_input' }
  let(:mock_response) { { key: 'value' } }

  subject { MyApi.new(api_key) }

  before do
    allow(subject).to receive(:call).with(input).and_return(mock_response)
  end

  it 'returns the expected response' do
    response = subject.call(input)
    expect(response).to eq(mock_response)
  end
end