Always Wrap Third-Party API Clients

Always Wrap Third-Party API Clients

Have you seen Ruby code like this?

And here are the tests:

(This is adapted from a real Slack bot that I wrote to help teammates publicly award each other at Hired.)

It doesn’t look so bad at first glance, but there are a few problems that show up on closer examination:

  1. The Slack code operates at a lower level of abstraction than the Rails controller code.
  2. If we need to do the same (or even a slightly different) thing elsewhere, it’s hard to reuse. Rails controllers are isolated from the rest of the app and don’t lend themselves well to object-oriented design principles.
  3. The testing is complex: we need to do some deep, chained stubbing in order to verify that Slack is running the right code.

The Wrong Abstraction

It can be hard to find the right abstraction for your code. In this case, there are a few levels of abstraction (as well as separate concerns):

* The controller code is high-level

* The Slack code is lower-level and requires the controller (and tests) to know a lot about the Slack API

Slack’s API client matches the Ruby methods (like chat_postMessage) to their URLs (like

[chat.postMessage](https://api.slack.com/methods/chat.postMessage)). But your application code doesn’t need to know about the Slack API’s URLs. It should operate at a higher level of abstraction, like “alert the current user”. To wrap that logic, create a new class with high-level methods that understands how to translate them into one or more Slack API calls.

That new class might look something like this:

And the controller now looks like this:

Let’s look at the new tests:

This test is now separated from the details of interacting with Slack’s API. If we decide to alert the user in a different way, or if the Slack API changes, this test does not need to change at all. SlackClient‘s internals will change, and the tests for SlackClient will change, but the controller tests are correctly shielded from that churn.

This fixes each of the problems listed above:

  1. The new code operates at a high level of abstraction now. The code talks about “alerting the user”, not specific API calls.
  2. The Slack code is in its own class and can be reused anywhere in the app without the controller’s baggage.
  3. The tests are easier to write and maintain. When testing the controller, the SlackClient code can be stubbed out without any references to chat_postMessage. When testing the Slack client, there’s no controller dependency at all so we can easily unit test it.

There’s an additional benefit as well: refactoring will be easier since the Slack code is centralized in SlackClient. If we add caching or other optimizations, it will benefit everywhere that interacts with Slack. If we scattered direct Slack::Web::Client calls all over, it would be much harder to find all of them and refactor in a few months.

Don’t forget: if you’re using a 3rd-party API client, don’t wait: wrap it in your own class on first use!

candidate-banner-2