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.
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.
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.
Moxto 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.
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
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
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:
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.
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 (
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
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
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.
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.
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.