Blueprint for Creating Mocks of External Services in Elixir
The guys aren’t using “mock” as a noun.
I’ve been writing a lot of “grown up” Elixir code recently which inevitably leads to (1) interacting with external services and (2) writing tests to exercise those interactions.
Coming from Ruby & Javascript land, my first instinct was to find out what mocking library one uses in Elixir. Just as the many others who’ve trod this path before me, I quickly found José’s blog post on “Mocks and explicit contracts”. I internalized the “mock as a noun” mentality and pressed ahead…
Now, a few weeks later, I’ve done this a couple of times and I have a reasonable blueprint for creating a mock of an external service.
The tl;dr
For the impatient (read: me, in 2 weeks), here is the top-level process I’ve been going through:
- Create a thin wrapper module around your external service client.
- Use
Mox
to make a mock based on your wrapper available in your test suite. - Add callbacks for any functions in the wrapper that you want to be included in the mock.
- Update the function under test to take an additional argument - the client module to the external lib. This makes the dependency injection easier.
- Update your test to use the mock.
The Elaboration
To elaborate on the list above, I’m going to create an example app. It’s purpose is to take a given word and check it against an obscenity blocklist. The blocklist is stored as a set in Redis. Let’s start with a module that looks like this:
This code works fine, but once we start to write tests for it, the discomfort begins. Sure, we could have a test instance of Redis but the point of check_word/1
is to return true
or false
based on the result of the SISMEMBER
call to redis (as well as to remember the key).
Wrap up those external calls
The first step is to stop calling the external service client (Redix
in this case) directly from our module. To do this, we’ll wrap it in a new module:
and update our check_word/1
function:
Cool. Now we’ve got an interface to our external library that all of our application code can use. This also sets us up for a later step in this blueprint. But first let’s:
Setup Mox
Mox is a library that simplifies the process laid out in the aforementioned Platafomatec blog post. From said post:
Almost 2 years later we have released a tiny library called Mox for Elixir that follows the guidelines written in this article.
Add the dependency to your mix.exs
file as per usual and run mix deps.get
. Once that finishes we need to define a mock for our tests. The simplest way to do this is to add a line to your test/test_helper.exs
file. In our case we’ll add this line:
Great! Now that our tests are ready to define a mock, we need to define the behaviours that our mock exposes.
Add callbacks
The way we define what functions our mock “knows” about is through callbacks. In our example, we want our mock to expose the ismember
function so we’d add a callback to our RedisClient
wrapper like:
To be honest, my types are a bit verbose in this example - you could get way with replacing them all with term()
and be fine. The point is that this callback defines a behaviour for our mock (RedisClientMock
).
Okay, so now we have a behaviour that our mock will implement. The next step is to update our client code to have its dependency (either RedisClient
or RedisClientMock
) injected.
Inject our dependency
In our current implementation of MockApp.Blocklist.check_word/1
we assume that we will me calling ismember
on our RedisClient
module. Let’s make this function more flexible by adding another argument that represents the redis client we want the function to use and default it to RedisClient
With the dependency now being passed into the function - let’s write the test!
Write the test with the mock
Now comes the part we’ve been waiting for. For our example, the test could look like this:
In this example, I don’t really need to assert anything on the args passed to ismember
but I wanted to show that you could.
That’s it
At this point you’ve got a situation where
- All your calls to an external service go through a module you control.
- The functions that need to call the external service have that module passed in as an argument.
- You have a mock to allows you to test your functions without actually calling out to the external service.
And that’s a situation I like to be in.
Disclosure
I wrote this post mostly as a reference for me (and maybe my team) so I didn’t discuss higher level concepts, like “what’s a mock?”, “why use a mock?”, what if I’m using a framework I don’t control and can’t use dependency injection?”
If that’s the kind of information you need, I suggest looking at the Platafomatec blog post referenced above in addition to this post from Carbon Five.