Intro
Integrating external services in web applications has been around for a long time now. We often use various OAuth providers such as Facebook, Twitter, GitHub and other alternatives for user signup, depending on our needs. Other times, things are more complicated. Maybe we are using Stripe or PayPal for online payments, or maybe we are dealing with a service-oriented architecture, where multiple services (or web applications) are interconnected.
Our applications need to be tested, and we need to make sure they will work with these services. Our production code communicates with the services, but issuing actual HTTP calls to them in test environment is almost always a bottleneck. Sometimes it’s just slow due to network traffic or low bandwidth. Other times, we can get unexpected failures due to the service itself. Having our test suite isolated from external services or dependencies is a good testing practice. This is why we should resort to stubbing them.
The Idea Behind Stubbing Services
The idea behind stubbing dependencies or services is quite simple. For example, we are building a CRM application where users can issue invoices to customers. One of the requirements is that the user (the invoice issuer) can change the currency of the invoice and have the costs automatically converted from the old into the new currency. To avoid any human errors, we want the application to resort to a currency converter API, for example www.fixer.io’s API, which will convert the currency amount. Our code should communicate with this API when needed and apply the results of the conversion in the invoice.
Our production code communicates with the API, but, to avoid any of the aforementioned downsides, we need to make sure that it should not send actual HTTP requests to the API while the tests are running. Stubbing lets us capture the call to the API and return a fake response with the look and feel of a real API response. The code continues working with the response data without actually calling the API.
In code, this would look something like this:
class Converter
def initialize(amount, source = "EUR", target = "USD")
@amount = amount
@target = target
@source = source
end
def convert!
body = get_exchange_rate_from_api
rate = extract_exchange_rate(body)
@amount * rate
end
private
def extract_exchange_rate(body)
JSON.parse(body)["rates"][@target]
end
def get_exchange_rate_from_api
url = URI(api_url)
Net::HTTP.get(url)
end
def api_url
"http://api.fixer.io/latest?symbols=#{@target}&base=#{@source}"
end
end
This is a quite contrived, but working example that communicates with the Fixer.io API. For example, it does not have proper error handling, but it is good enough for the purpose of this tutorial.
Let’s take a look at some popular approaches to stubbing external dependencies or services in Rails.
Approaches to Stubbing
In this tutorial, we will cover the three most popular approaches to stubbing external services in Rails applications – Webmock, VCR, and writing a fake service.
Webmock
Webmock is a “library for stubbing and setting expectations on HTTP requests in Ruby”. It allows us to stub HTTP requests and to set and verify expectations on any HTTP requests. It’s very easy to use with the most popular testing frameworks for Ruby – Test::Unit
, RSpec
and Minitest
.
Setting up Webmock
is simple. Visit its README for installation instructions. The installation usually consists of installing the gem and requiring it in the test_helper
or spec_helper
file. For Minitest, the installation looks as follows:
# test_helper.rb
require 'webmock/minitest'
And for RSpec:
# spec_helper.rb
require 'webmock/rspec'
Let’s write a test in Minitest
and stub the API call using Webmock
:
class ConverterTest < Minitest::Test
def setup
stub_request(:get, "http://api.fixer.io/latest?symbols=USD&base=EUR").
to_return(:body => %Q(
{
"base": "EUR",
"date": "2015-12-14",
"rates": {
"USD": 2.0
}
}
))
end
def it_converts_eur_to_usd
assert_equal 4, Converter.new(2, "EUR", "USD").convert!
end
end
Let’s see what happens in the test. In the #setup
method, we use Webmock
to stub the request. This means that every time the Converter#convert!
method is called and it tries to reach out to the API via an HTTP call, Webmock
will “trap” the request and return the predefined body. This is used to isolate the tests from the actual API.
As you can notice in the assertion, expect 2 Euros to be 4 US Dollars. Although this is not the correct exchange rate, we’ll use these numbers to make things simple.
Webmock gives us the flexibility to define our own response body and use it in our tests. However, this flexibility can often be a disadvantage. The problem mostly arises if the endpoint changes, or the structure and/or data of the response is changed. Then, our test data is not synced with the real API response, and we need to manually update the stubbed responses.
VCR
Another popular approach to stubbing out external services is VCR. The difference between Webmock and VCR is that with Webmock you can explicitly define the response you would like to receive, and VCR records the HTTP requests. This means that VCR will save all of the HTTP traffic in a YAML file, and every time you try to issue the same HTTP call, it will replay it.
Setting up VCR quite easy, just like with Webmock. After you have installed the gem, you need to add the following lines in the test_helper.rb
or spec_helper.rb
file:
require 'vcr'
VCR.configure do |config|
config.cassette_library_dir = "fixtures/vcr_cassettes"
config.hook_into :webmock # or :fakeweb
end
The first line just requires VCR so you can use it in the test suite. The lines that follow are configurations for VCR. The first line in the configure block sets the path where the YAML files will be saved, relative to the spec
or test
directory in the project root path. The second line tells VCR to use Webmock under the hood, so it can stub the requests with the recorded HTTP traffic when running the tests.
Using VCR in a test is really easy. Let’s write a test using Minitest and VCR:
class ConverterTest < Minitest::Test
def it_converts_eur_to_usd
VCR.use_cassette("eur_to_usd_conversion") do
assert_equals 4, Converter.convert!(4.3932, "EUR", "USD")
end
end
end
As you can see, we added some readability to our test, but some of the functionality is hidden behind VCR. We pass the cassette name to the VCR.use_cassette
method as an argument. This means that when VCR first runs the test, it will make an actual HTTP call to the API. Then, it will save all of the data from the HTTP call to a file called eur_to_usd_conversion.yml
in the test/fixtures/vcr_cassettes
directory. Here’s what the file looks like:
# test/fixtures/vcr_cassettes/eur_to_usd_conversion.yml
---
http_interactions:
- request:
method: get
uri: http://api.fixer.io/latest?base=EUR&symbols=USD
body:
encoding: US-ASCII
string: ''
headers:
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
User-Agent:
- Ruby
Host:
- api.fixer.io
response:
status:
code: 200
message: OK
headers:
Server:
- nginx/1.4.6 (Ubuntu)
Date:
- Tue, 15 Dec 2015 23:00:20 GMT
Content-Type:
- application/json
Content-Length:
- '56'
Connection:
- keep-alive
Status:
- 200 OK
Last-Modified:
- Tue, 15 Dec 2015 00:00:00 GMT
X-Content-Type-Options:
- nosniff
body:
encoding: UTF-8
string: '{"base":"EUR","date":"2015-12-15","rates":{"USD":1.099}}'
http_version:
recorded_at: Tue, 15 Dec 2015 23:00:20 GMT
recorded_with: VCR 3.0.0
As you can see above, the file holds all of the data for the HTTP request and its response. If we rerun the test, this time it will return the response of the API call without sending an actual HTTP request, because it already has all the data it needs in the YAML file. This is useful because you can reuse the file in a different test in order to issue the same HTTP request.
When it comes to the disadvantages of using VCR, they basically overlap with Webmock’s disadvantages, i.e. going out of sync with the real API responses. Fortunately, it has a nice automatic re-recording mechanism, which re-records the cassettes after a certain time period.
Dependency Injection
Another interesting approach to stubbing external services is dependency injection (DI). DI is a software design pattern that helps with dependency inversion. Let’s see an example of Dependency Injection to understand it better. We will refactor the Converter
class:
class Converter
def initialize(amount, source = "EUR", target = "USD", api = FixerAPI)
@amount = amount
@target = target
@source = source
@api = api
end
def convert!
rate = @api.get_exhange_rate(source: @source, target: @target)
@amount * rate
end
end
In the constructor of the Converter
class we inject the FixerAPI
class. This object is a service that will be a wrapper for the Fixer API. It will communicate with the API, and it will parse the response body, returning meaningful data through well-named methods.
class FixerAPI
def self.get_exchange_rate(source: source, target: target)
new.get_exhange_rate(source: source, target: target)
end
def get_exchange_rate(source: source, target: target)
url = URI(api_url(source, target))
body = Net::HTTP.get(url)
JSON.parse(body)["rates"][target]
end
private
def api_url(source, target)
"http://api.fixer.io/latest?symbols=#{target}&base=#{source}"
end
end
As you can see, the FixerAPI
class has a convenient method which will return the current conversion rate between two currencies. The FixerAPI
is a dependency to Converter
, which is being injected in the constructor. By having the service injected, it can easily be swapped with another service which has the same interface as the FixerAPI
class.
So, how can we use dependency injection to isolate the service? Instead of using VCR
or Webmock
, we can inject a different “fake” service. It has the same signature as the real service, but it will return a hardcoded response, similar to Webmock
.
class FakeAPI
def self.get_exchange_rate(source: source, target: target)
2
end
end
class ConverterTest < Minitest::Test
def it_converts_eur_to_usd
assert_equals 4, Converter.convert!(2, "EUR", "USD", FakeAPI)
end
end
A common disadvantage of using dependency injection is the overhead that comes with it. Whenever we want to add DI to a class, we need to think about it in advance, and adapt our design to it. Also, the service class that will be injected has to be properly tested, so we might face the same challenges as with Webmock and VCR in the end.
Fake Service
Writing a fake service is not a common approach to this problem. However, it is still a solid approach to solving the problems with stubbing services and/or dependencies.
In cases like this one, a tiny Sinatra application is usually sufficient. We would need an endpoint which would mimic the actual API. One drawback is that using this technique for stubbing can create inconsistencies in the long term. While the production code would work with the fake service in the test environment, changing the data or the structure of the data that is returned by the service would cause the service classes to fail in production, but not in the test environment. This is something to keep in mind, and it is probably the reason why this approach to stubbing is unpopular.
The code of the fake service written with Sinatra.rb would look as follows:
require 'sinatra'
get '/latest' do
content_type :json
{ base: "EUR", date: "2015-12-15", rates: { usd: 1.099 } }.to_json
end
As you can see, although this is quite easy to write, we will need to make changes to our service class, more specifically to the api_url
method:
class FixerAPI
def self.get_exchange_rate(source: source, target: target)
new.get_exhange_rate(source: source, target: target)
end
def get_exchange_rate(source: source, target: target)
url = URI(api_url(source, target))
body = Net::HTTP.get(url)
JSON.parse(body)["rates"][target]
end
private
def api_url(source, target)
base_url = ENV['FAKE_FIXER_URL'] || "http://api.fixer.io"
base_url + "/latest?symbols=#{target}&base=#{source}"
end
end
To make the service class work with the fake service, we will need to set the URL of the local fake service via an environment variable. Using this method, we can easily configure the service class to work with our fake service.
Weighing Different Options
As we saw in this tutorial, there are multiple approaches to stubbing external dependencies and/or services. Whether you choose to use Webmock
, VCR
, dependency injection, or write your own fake service, isolating your tests from the actual service is a good practice.
However, as with any other practice, there are some disadvantages. At its core, stubbing means predefining responses to calls to the service. This means that the responses that you (or a gem like VCR) have defined are stored locally, either in your code, or in a YAML file.
So, a good question to ask is — what will happen when a service response changes? Unfortunately, this change will not be picked up by your tests, because the tests use the stubbed data. Solving this issue in testing is an ongoing issue, and still has not been resolved completely. Compared to the other approaches, VCR has a nice mechanism of automatic re-recording of the cassettes that we mentioned, so they get updated after a certain time period that can be defined.
What approaches to stubbing do you use? And what are your experiences with stubbing in general? Feel free to share them in the comments.
References
Here are some of the links used for writing this tutorial:
P.S. Would you like to learn how to build sustainable Rails apps and ship more often? We’ve recently published an ebook covering just that — “Rails Testing Handbook”. Learn more and download a free copy.