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
orNet::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