Testing Third Party Interactions

Joël Quenneville

When it comes to testing interactions with third-party APIs, it there are a bewildering set of approaches and tools to work with. We’ve written several articles listing various approaches as well as devoting a chapter to it in our new book Testing Rails. So how do you know which one to use?

Taking a closer look at these, it turns out they all fall into one of two testing approaches testing on one of two levels. These form a 2x2 matrix

(Adapter | HTTP) x (Stubbed | Real)

Stubbing the adapter

You have an object that encapsulates your third party interactions. In tests that interact with that object, you stub it so that it doesn’t really make a network request but instead returns a canned value.

For example, you are testing a shopping cart’s tax calculation functionality but the prices for the items are dynamic and are calculated by a separate quote service:

require "rails_helper"

describe ShoppingCart do
  it "calculates tax" do
    item = create(:item)
    shopping_cart = ShoppingCart.new(item)

    allow(QuoteService).to receive(:fetch).and_return(100)

    expect(shopping_cart.tax).to eq 10
  end
end

This is particularly useful when unit-testing objects that depend on data from other services.

Beware of stubbing the system under test! Don’t stub QuoteService when testing QuoteService.

In general, you also want to avoid stubbing in an integration/feature spec since those try to test the whole stack and you probably want the QuoteService to be tested as part of your end-to-end testing.

Stubbing the network

So what if you want the QuoteService to execute but don’t want to actually make a network request? Stubbing the network is a popular way to do this. The idea is the QuoteService thinks it’s making HTTP requests but in reality it is being given canned answers.

There are many ways of accomplishing this:

  • Manually stubbing the HTTP library (e.g. HTTParty or Net::Http)
  • Using a gem like Webmock which returns arbitrary values when making HTTP requests
  • Using a gem like VCR that records the real response of your API and then replays it instead of making a real HTTP request.

For example, here we use Webmock to stub the HTTP layer. Note that this test exercises both the ShoppingCart and the QuoteService so it’s not a pure unit test.

require "rails_helper"

describe ShoppingCart do
  it "calculates tax" do
    item = create(:item)
    shopping_cart = ShoppingCart.new(item)

    stub_request("http://quote-service.com/prices").and_return(
      { pricesRequest: { regularPrice: 100 } }.to_json
    )

    expect(shopping_cart.tax).to eq 10
  end
end

This approach is particularly helpful in integration/feature specs since you can exercise the whole system without worrying about making HTTP requests.

Writing a bunch of ad-hoc stubs can get messy if you have a lot of them. This approach works best in one-off request situations.

Swapping out the adapter

Instead of stubbing the your domain object that handles interactions with the third party API, this approach drops in a test version that doesn’t make HTTP requests instead.

For example, if you’re testing 2-factor auth and don’t want to actually send out SMS in your test but also want to assert on the texts sent, you might write a test like:

feature "signing in" do
  scenario "with two factors" do
    create(:user, password: "password", email: "user@example.com")

    visit root_path
    click_on "Sign In"

    fill_in :email, with: "user@example.com"
    fill_in :password, with: "password"
    click_on "Submit"

    last_message = FakeSMS.messages.last
    fill_in :code, with: last_message.body
    click_on "Submit"

    expect(page).to have_content("Sign out")
  end
end

FakeSMS is a drop-in replacement for your regular TwilioSMS class that would actually send messages over the wire but FakeSMS stores the messages in memory instead. This is a special take on the strategy pattern

This approach is useful in integration/feature specs, particularly when you need to assert on data that has been sent or received from the service.

Note that because we’re using the FakeSMS adapter instead of the TwilioAdapter, the code in TwilioAdapter doesn’t get exercised in our test.

We go more in depth on using this approach this article on testing SMS.

Swapping out the server

All the previous approaches have had one flaw: you can’t fully test your application from end to end. There’s always some small part that gets stubbed or swapped to prevent actual HTTP requests from taking place. Generally that’s OK as long as you are aware of the tradeoffs.

This final approach asks “what if we didn’t try to prevent HTTP?”. Instead of trying to prevent HTTP, you make real HTTP requests to a local server.

You write up a small fake service (perhaps using Sinatra) and then configure your application to point to your local service in tests rather than the real one. These fakes can be shared and reused. For example, we’ve created the fake_stripe gem that provides a fake for the Stripe payment service.

In addition to allowing full end-to-end testing of your app, this approach scales nicely too. If you have a process that requires a lot of requests, such as OAuth or some other token exchange auth system, working with a fake server is a clean way to deal with all those requests going back and forth.

We’ve written about this approach in depth before. In addition to allowing you to test fully end-to-end, the fake local server you create can be reused as a local or staging sandbox.

The Weekly Iteration

This article is inspired by an interview on the with Chris Toomey on The Weekly Iteration