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
- We can either use
get
orget_response
; if using the latter, then.body
needs to be called on thetext_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