Using Webmock For Robust HTTP Request Testing In Ruby

Ever integrated your app with third party APIs? You know, like sending request to Google Cloud storage or connecting to a Trello board? To ensure that our integration mechanism work fine, it is always better to write automated tests for it. If you have tried writing test for it, you will notice that from the test log that your spec/test will trigger http requests to the said APIs. This is the desired outcome, as this means that the integration works perfectly fine. However, this slows down your test suite considerably and you probably do not want your test suite to touch the APIs all the time as it can affect any data you store for production purposes. The solution? Stubbing the http request in test, once you have confirmed that the integration is okay manually.

To ensure better coverage, it is better to use stubbing/mocking techniques that focuses more on whether the client interacts with other part of your codes (controller/service/model) as expected, rather than checking whether the client works or not. This is the approach that I personally recommend as it allows you to focus on the integration of your codes. It does not matter that your client passed the test 90-100% of the time you run it if you do not write test for the part of codes that use the responses from the client. You can do this in a number of ways, like using Rspec double, but in this post I will demonstrate using a gem called 'Webmock' to stub external requests.

This post assumes you are using Rails and rspec, so start by adding Webmock to the gemfile:

# Gemfile
gem 'webmock'

then run bundle install. After that, start integrate webmock to your test by adding this line:

# spec/rails_helper.rb

require 'webmock/rspec'
WebMock.disable_net_connect!(allow_localhost: true)

The last line above basically prevents your test from sending ALL HTTP requests that may be triggerred by the test run. This means that if previously you already have a part of the test suite that triggers HTTP request, those tests may fail now that you block HTTP request and you have to do some bit of adjusment.

Now, given this spec file that basically asks the Xendit API client to send request to get the account balance:

# spec/lib/xendit/client.rb

    describe '.get_balance' do
      context 'valid request' do
        it 'should return the current balance of the merchant account' do
          api_key = ENV['XENDIT_API_KEY']
          client =  Client.new(api_key: api_key)

          result = client.get_cash_balance 

          expect(result.balance).to eq 1241231
          expect(result.balance_cents).to eq 124123100
        end
      end
    end

let's get down to the business and start introducing the stubs. There are basically two ways to do this: 1) just add it to the rails_helper.rb and load them all everytime the test suite runs, or 2) introduce the stubs on as needed basis.

The first approach looks like this:

# spec/rails_helper.rb

RSpec.configure do |config|
  config.before :each do
    # Stubs fetching balance request to Xendit API
    stub_request(:get, "https://api.xendit.co/balance")
            .to_return(:status => 200, :body => '{"balance": 1241231}', :headers => {}) 
  end    
end

the test now will pass. But I do not recommend this approach as it is inflexible. For example, with tis way, you can't really simulate what would happen if the request fails to generate success response. So how do we implement it using the second (better) approach? First, remove the stub_request line on the rails_helper.rb then:

describe '.get_balance' do
  context 'valid request' do
    before do
      @stub = stub_request(:get, "https://api.xendit.co/balance")
        .to_return(:status => 200, :body => '{"balance": 1241231}', :headers => {})          
    end

    it 'should return the current balance of the merchant account' do
      api_key = ENV['XENDIT_API_KEY']
      client =  Client.new(api_key: api_key)

      result = client.get_cash_balance 

      expect(result.balance).to eq 1241231
      expect(result.balance_cents).to eq 124123100
      expect(@stub).to have_been_requested
    end
  end
end

Notice also an extra benefit of using this approach is that you can test exactly whether 1) the expected request has been made and 2) how many times that said request has been made, by using this line:

# This is just to check for the request
expect(@stub).to have_been_requested

# This is for also checking the frequecy
expect(@stub).to have_been_requested.times(3)

# If you expect the request has not to be made
expect(@stub).to_not have_been_requested

This is especially useful in integration test when you want to make sure that a particular function should send an exact number of request to a particular endpoint. For example, in my case I was testing a class that registers all students of an online classroom to an asyncrhonous meeting room using Zoom.us API. For this function to work as per requirement, all students have to be registered. So in the test, I expected that for a classroom that has 5 students, there have to be 5 stubbed HTTP requests to the correct Zoom.us API endpoint.

Basically that is all you need to do stubbing using Webmock. However, I want to go one step further and recommend you to incorporate using file fixture when stubbing with Webmock. Notice that in the example that my stub is quite short:

@stub = stub_request(:get, "https://api.xendit.co/balance")
          .to_return(:status => 200, :body => '{"balance": 1241231}', :headers => {})

Now this is because that particular endpoint only returns one parameter in the body as the response, but this might be longer in your use case and I certainly have written stubs where the argument for the keyword :body spans a number of lines such as this. The solution to keep your test code to stay elegant is by using file fixture. So, do this:

Create fixtures folder inside the spec folder (if you haven't used fixtures already) and add files folder inside it and then write the .json file that contains the stubbed response:

# spec/files/xendit_get_balance.json

{
    "balance": 1241231
}

This step is exclusively for Rails 4 because in the Rails 5, this method already exists and do the same thing:

# spec/rails_helper.rb

# In the real world, you should put this inside a support file and then load it on the spec suite

def file_fixture(filename)
  url = File.join(Rails.root, 'spec', 'fixtures', 'files', filename)

  File.read(url)
end

Now, just call it on the stub

  @stub = stub_request(:get, "https://api.xendit.co/balance")
    .to_return(:status => 200, :body => file_fixture('xendit_get_balance').as_json, :headers => {})

And that is that basically. Your test suite is now more robust and stays elegant, hopefully. Cheers!