Stubbing external services in rails

Stubbing External Services in Rails

Learn how to stub external services when testing your Ruby on Rails application.

Brought to you by

Semaphore

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. Semaphore is working on a book "The Ultimate Guide to BDD with Rails". Sign up to receive a FREE copy.

81b70354d05caf6bdf7e7953713c1cd4
Ilija Eftimov

Full-stack Ruby on Rails developer. Blogs regularly at eftimov.net.

on this tutorial so far.
User deleted author {{comment.createdAt}}

Edited on {{comment.updatedAt}}

Cancel

Sign In You must be logged in to comment.