Here's How Inverse Tests External APIs in Elixir with Bypass
We're testing external API using Bypass in our Service Oriented Architecture.
We prioritize Service Oriented Architecture principles at Inverse. That means we have small, maintainable components with clearly defined responsibilities. They communicate with one another (mostly), via Representational State Transfer, or REST, APIs.
This provides flexibility and has served us well with the exception of one significant facet: Testing. When testing, one should avoid:
- Dependence on external services running on the same machine.
- Slow tests.
Because applications inherently rely on external services, it is critical to have a testing strategy in place for those dependencies.
We recently started using Bypass and I will explain how we arrived there and specifically how we are using it.
The Past
Mock methods and return some example data like this:
That was (and I believe still is) the “way to go” in the world of Ruby/Rails. Unfortunately, this fosters bad behavior as best explained here by José Valim.
We then started using ExVCR, which is a great library, but has similar drawbacks as mocks/stubs: It encourages laziness and doesn’t foster the separation of concerns that are critical to well defined APIs. ExVCR enables one to “record” and “playback” real-live data. It’s very easy to integrate (including a few lines in your test and everything else is taken care of). But ideally you have to think about external dependencies in tests, not abstract them out. It might still be a viable choice for scenarios when the endpoint behavior should be tested with minimal overhead (we use it for testing calls to Amazon’s AWS Services like S3).
Enter Adapters
Adapters work great and promote deliberation around API contracts and clearly defined communication boundaries. We still use this approach, especially when the Adapter is more complex (like a JSON-RPC socket).
This is how an Adapter might look:
But for simple HTTP Endpoints, Adapters seem like a lot of work and have a major drawback: They leave the libraries they consume out of the testing equation. If anything in the HTTP or JSON libraries changes, the tests won’t catch it. The amount of production-critical code that is left untested by this approach is unacceptable.
The Present and Future
Bypass allows us to start a very simple web server in tests that simulate external services we use.
Now, we can test the entire stack, including the HTTP library, JSON encoding/decoding library, and authentication mechanisms. The Bypass README is well written, so I will spare implementation details. We do, however, slightly change how we use it in order to keep tests concise and readable:
First off, we do sometimes want to call out to Facebook when tests are run as a full integration suite. We do this irregularly to ensure the Facebook API still functions per our expectations. Adding --include integration
to mix test
does not simulate the API but, instead, calls out to the external service (lines 5, 7).
We are explicit when we simulate requests to external services so that each test that uses Bypass must have the @tag facebook_bypass
(line 7).
Finally, the handle_fb
function (lines 30–39) is being called (given that the request_path
matches). I like matching in the function head as it makes explicit which path we are reacting to and allows us to define different functions for different paths.
So Bypass runs on only tests tagged with @tag :bypass
and when we are not running our integration suite. One more thing we do while setting up Bypass is allowing the tag to pass a page id (lines 8, 20). So here is how a test which uses Bypass looks in all its glory:
As you can see, the facebook_bypass
tag makes it explicit that we are simulating the API (unless we are in integration mode). It allows us to pass information to the simulated API, and it’s very easy to reuse the same Bypass config for different tests.
I hope this helps you test external APIs. You can find me on Twitter (see below) if you have any further questions.