NOT using mock as a noun 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:

  1. Create a thin wrapper module around your external service client.
  2. Use Mox to make a mock based on your wrapper available in your test suite.
  3. Add callbacks for any functions in the wrapper that you want to be included in the mock.
  4. Update the function under test to take an additional argument - the client module to the external lib. This makes the dependency injection easier.
  5. 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:

defmodule MockApp.Blocklist do
  def check_word(word) do
    key = "obscenity_blocklist"

    case Redix.command(:redis, ["SISMEMBER", key, word]) do
      {:ok, 1} -> true
      {:ok, 0} -> false
    end
  end
end

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:

defmodule MockApp.RedisClient do
  def ismember(conn, key, value) do
    Redix.command(conn, ["SISMEMBER", key, value])
  end
end

and update our check_word/1 function:

defmodule MockApp.Blocklist do
  alias MockApp.RedisClient

  def check_word(word) do
    key = "obscenity_blocklist"

    case RedisClient.ismember(:redis, key, word) do
      {:ok, 1} -> true
      {:ok, 0} -> false
    end
  end
end

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:

Mox.defmock(MockApp.RedisClientMock, for: MockApp.RedisClient)

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:

defmodule MockApp.RedisClient do

  @callback sismember(atom() | pid(), String.t(), String.t() | integer()) ::
                {:ok, Redix.Protocol.redis_value()} |
                {:error, atom | Redix.Error.t()}

  def ismember(conn, key, value) do
    Redix.command(conn, ["SISMEMBER", key, value])
  end
end

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

defmodule MockApp.Blocklist do
  alias MockApp.RedisClient

  def check_word(word, redis_client \\ RedisClient) do
    key = "obscenity_blocklist"

    case redis_client.ismember(:redis, key, word) do
      {:ok, 1} -> true
      {:ok, 0} -> false
    end
  end
end

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:

defmodule MockAppTest.BlocklistTest do
  use ExUnit.Case
  alias MockApp.{RedisClientMock, Blocklist}

  import Mox

  setup :verify_on_exit!

  describe "Blocklist.check_word/2" do
    test "it returns true if the given word is on the obscenity blocklist" do
      RedisClientMock
      |> expect(:ismember, fn conn, key, value -> # args passed to the ismember function
        assert(value == "darn") # any assertions you want to make on the args
        {:ok, 0}  # return value from the ismember call
      end)

      assert(Blocklist.check_word("darn") == false)
    end
  end
end

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

  1. All your calls to an external service go through a module you control.
  2. The functions that need to call the external service have that module passed in as an argument.
  3. 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.