No More Seat Costs: Semaphore Plans Just Got Better!

    27 Jan 2016 · Software Engineering

    Stubbing External Services in Rails

    12 min read
    Contents

    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.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    Full-stack Ruby on Rails developer. Blogs regularly at eftimov.net.