When integrating 3rd-party APIs, it’s important to listen to your tests. The most common design relies on tests that directly mock the 3rd-party API. These tests are brittle, but are often acceptable because, well, they work. A better approach is to take the time to design an app-specific interface to wrap the 3rd-party API. The resulting tests will be more resilient to change and more maintainable. Introducing an explicit interface for an implicit concept will also enrich your domain model. In this post, we’ll take a look at each of these approaches.
Our client has requested that their site display a realtime list of all currently signed in users. This list should be visible on every page on the site, and updated whenever a user signs in or out. For realtime support, we’ll use Pusher. Serverside events will be published to pusherapp.com using the Pusher Ruby gem; Pusher will then forward these events to browser clients via HTML5 WebSockets.
We’ll work on the first part of this feature: publishing serverside events to pusherapp.com. Let’s start with an integration test to capture the full end-to-end behavior.
This test signs in as an existing user, and then verifies that an HTTP request was made to pusherapp.com. The body of this HTTP request contains the event name, channel, and data (the signed in user’s email).
User registration and authentication are already implemented in the app via devise. So we’ll use devise’s
#after_sign_in_path_for hook to tell the currently signed in user to publish a “sign in” event.
With this basic implementation in place, we can drop down to the domain model and spec out
In our first pass, we’ll mock types from the Pusher gem.
Since we didn’t write the Pusher gem, we’re forced to mock its rather generic interface. If we upgrade the Pusher gem to a new version that’s not backwards compatible, we’ll have to update these tests. These fragile tests are telling us that there’s a better design; that we’re missing some concept. Let’s refactor this by introducing our own app-specific interface for event publishing.
Instead of mocking types from the Pusher gem, let’s create an object to wrap them: an event publisher. An event publisher will have a
#publish method that takes event information, and publishes it.
User#publish_sign_in will now take an event publisher and then delegate the event publishing to it.
By defining our own interface, an interface that will change on our app’s terms, our tests are simpler and more robust. The event publisher also enriches our domain model by making the act of event publishing an explicit concept.
Our integration test is still failing because we didn’t yet create an event publisher in the controller. In order to keep the controller simple, let’s use a default argument for the event publisher.
PusherEventPublisher will wrap the Pusher gem.
In this spec, we used vcr to record an HTTP request, and then verified its body with webmock. By stubbing at the HTTP level, this test is less vulnerable to changes in the Pusher gem. In fact, since we avoided mocking the Pusher gem, we could replace it without breaking this test.
And now our integration test is passing.
Every codebase will eventually have to use 3rd-party APIs. If your tests for these integration points are starting to hurt, then you most likely don’t have the right design. It’s no coincidence that well designed code is easy to test. So, instead of living with painful tests, take a step back, and consider a better approach.