An Introduction to Mocking Tools for Elixir
Tags: elixir
Published: 2023-04-18 11:09:36

A well-written test suite is a big part of any successful application. But let's say you rely on an external dependency for some parts of your app (for example, an external API for fetching user information). It then becomes important to mock that dependency in the test suite to prevent external API calls during testing or to test specific behavior.

Several frameworks help reduce the boilerplate and make mocking safe for Elixir tests. We will explore some of the major mocking tools available in this post.

Let's get started!

## Test Without Mocking Tools in Elixir

Depending on the scope of your tests, you might not need to use any external mocking tools. You can use test stubs or roll your own server instead.

### Test Stubs

The simplest way to stub/mock out some parts from a function call is to pass around modules that do the actual work and use a different implementation during the tests.

Let’s say you need to access the GitHub API to fetch a user’s profile with an implementation like this:

```elixir
defmodule GithubAPI do
  def fetch_user(username) do
    case HTTPoison.get("https://api.github.com/users/#{username}") do
      {:ok, response} ->
        parse(response)
      {:error, reason} ->
        {:error, reason}
    end
  end
end
```

We know the implementation relies on HTTPoison to make actual calls to the GitHub API. To mock it during tests, we can update the implementation to pass around an `http_client` and use a different one during tests.

Something like this:

```elixir
defmodule GithubAPI do
  def fetch_user(username, http_client \\ HTTPoison) do
    case http_client.get("https://api.github.com/users/#{username}") do
      #... handle results
    end
  end
end
```

This approach aligns with the [Law of Demeter](https://www.infoworld.com/article/3136224/demystifying-the-law-of-demeter-principle.html) — a module should only have limited knowledge of the objects (in this case, the HTTP client) it works on. This approach is nice because it decouples the `GithubAPI` module from the HTTP client implementation.

Our `GithubAPI` users can continue using it in the same way.
But for unit-testing `GithubAPI`, we can pass in our custom `http_client` that returns just the results we need.

For example:

```elixir
defmodule GithubAPITest do
  defmodule GithubHTTPClientUser do
    def get("https://api.github.com/users/octocat") do
      {:ok, %HTTPoison.Response{status_code: 200, body: ~s<{"login": "octocat"}>}}
    end
    def get("https://api.github.com/users/unknown") do
      {:error, %HTTPoison.Error{message: "Not Found"}}
    end
  end

  test "can fetch a user" do
    {:ok, %Github.User{login: "octocat"}} = GithubAPI.fetch_user("octocat", GithubHTTPClientUser)
  end

  test "handles errors when fetching a user" do
    {:error, %HTTPoison.Error{}} = GithubAPI.fetch_user("unknown", GithubHTTPClientUser)
  end
end
```

That works, and you have 100% test coverage! 🎉 But as you can already imagine, while this works well for small tests, it will become increasingly difficult to manage if our `GithubAPI` class grows to include other features.

Another similar strategy is to use application configuration instead of passing around the client modules. This is especially useful when you need to mock out the API from all the tests, even when this API is called from other internal modules.

We can update our code to this:

```elixir
# github_api.ex
defmodule GithubAPI do
  @http_client Application.compile_env(:my_app, GithubAPI, []) |> Keyword.get(:http_client, HTTPoison)

  def fetch_user(username) do
    case @http_client.get("https://api.github.com/users/#{username}") do
      #... handle results
    end
  end
end

# test/support/mock_github_http_client.ex
defmodule MockGithubHTTPClient do
    # ... All mock implementations here
end

# config/test.exs
config :my_app, GithubAPI, http_client: MockGithubHTTPClient
```

The good thing is that you don’t need to worry about mocking out `GithubAPI` calls in each test case by manually passing the HTTP client.

This is especially important when the actual API calls are nested inside other functions. For example, if your application automatically fetches the user from GitHub after a new account is created, you don’t need to mock `GithubAPI` everywhere you create new users.

But it still has the same disadvantages as the previous strategy. Plus, the mocks must be generic enough to be used throughout the test suite.

### Roll Your Own Server

This one is interesting. Since [plug and cowboy](https://github.com/elixir-plug/plug_cowboy) make it really easy to roll out an HTTP server, instead of mocking out the HTTP Client, we can start our own server during tests and respond with stubs instead.

If you want to learn more, check out:

- [Testing External Web Requests in Elixir? Roll Your Own Mock Server](https://medium.com/flatiron-labs/rolling-your-own-mock-server-for-testing-in-elixir-2cdb5ccdd1a0)
- [How to Test WebSocket Clients in Elixir with a Mock Server](https://blog.appsignal.com/2021/06/22/how-to-test-websocket-clients-in-elixir-with-a-mock-server.html)

In the strategies discussed above, quite a bit of boilerplate is involved in setting up the tests.
And if you need a way to validate that a specific function was called with specific arguments, you need to do additional work.

If you just have a small external dependency that you need to mock out, this might work well for you. But if there are complex edge cases and branches that you need to test out, mocking tools can help simplify the complexity of writing and maintaining those mocks in the long run.

## Elixir Mocking Tools

## Mock

[Mock](https://github.com/jjh42/mock) is the first result you will see when searching “Elixir Mock”, and is a wrapper around Erlang’s [meck](https://github.com/eproxus/meck) that provides easy mocking macros for Elixir.

With `Mock`, you can:

- Replace any module at will during tests to change return values.
- Pass through to the original function.
- Validate calls to the mocked functions.
- Check the complete call history, including arguments and results for each call.

It is a very powerful tool, and the macro [`with_mock`](https://github.com/jjh42/mock#with_mock---Mocking-a-single-module) makes mocking during tests really easy.

Let’s see how we can rewrite our test case to validate `GithubAPI.fetch_user`:

```elixir
defmodule GithubAPITest do
  # This is important, Mock doesn't work with async tests!
  use ExUnit.Case, async: false

  import Mock

  test "can fetch a user" do
    with_mock HTTPoison, [get: fn _url -> {:ok, %{status_code: 200, body: ~s({"login": "octocat"})}} end] do
      {:ok, %Github.User{login: "octocat"}} = GithubAPI.fetch_user("octocat")
      assert_called HTTPotion.get("https://api.github.com/users/octocat")
    end
  end
end
```

Very sleek. There's no need to define and pass boilerplate modules around or fiddle with the config just for tests. Our implementation is completely isolated from the test code and needs no special changes to fit the tests. And if we need to validate that a function was called with specific arguments, we can do that as well with `assert_called`.

One of the major drawbacks of this strategy is that you cannot use it with `async` tests. It doesn’t prevent you from using Mock inside asynchronous tests, so this can lead to hard-to-track flaky tests.

In my opinion, Mock works well for certain types of tests.
I usually find myself reaching for it when we need to mock external libraries that we have no control over. For example, let’s say we use an [external library](https://github.com/edgurgel/tentacat) to fetch a user’s GitHub profile instead of our custom `GithubAPI`. Something like this:

```elixir
def create_user(github_username) do
  Tentacat.Users.find(Tentacat.Client.new(), github_username)
  |> case do
    {:ok, user} ->
      # ... create user
    {:error, error} ->
      # ... handle error
  end
end
```

Now, if we dig into the library’s documentation/code, we can see that it uses HTTPoison to make the eventual request to the API. But we don’t want to — or have a way to — customize that client just for tests. Here, `with_mock` can easily help mock out that call so that we don’t request the API.

In fact, a much better approach, in this case, is to use `with_mock` to simply mock out the `Tentacat.Users.find` call altogether. That saves you from having to dig into the library's internal code (which can change with any update) and solely relying on mocking out its public interface.

## Mox

We saw above how easy it is to mock some methods out and have our tests pass.
We sprinkle these calls in a few places to mock `HTTPoison`, and we are done.

But what if we later decide that `HTTPoison` isn’t fast enough and want to switch to another HTTP client implementation? As expected, all our tests will fail — we have to go back and fix them.
Even worse, what if the API for `HTTPoison` changes, but since we mocked it out, our tests never failed, and we pushed something that didn’t work to production?

[Mox](https://github.com/dashbitco/mox) helps get around these issues by ensuring explicit contracts.
Read [Mocks and Explicit Contracts](https://dashbit.co/blog/mocks-and-explicit-contracts) for more details.

Using our `GithubAPI` example above, this is how we need to set up the tests with Mox.

### Mock the External Client

We first convert our API client into a [`Behaviour`](https://hexdocs.pm/elixir/1.4.5/behaviours.html) to define the explicit contract the API client should follow.

```elixir
# lib/my_app/github/api.ex
defmodule MyApp.GithubAPI do
  @callback fetch_user(String.t()) :: {:ok, Github.User.t()} | {:error, Github.Error.t()}

  def fetch_user(username), do: impl().fetch_user(username) do

  defp impl(), do: Application.get_env(:my_app, GithubAPI, GtihubAPI.HTTP)
end
```

Next, we define the API Client that makes the actual request to fetch the user.

```elixir
# lib/my_app/github/http.ex
defmodule MyApp.GithubAPI.HTTP do
  @behaviour MyApp.GithubAPI

  @impl true
  def fetch_user(username) do
    case HTTPoison.get("https://api.github.com/users/#{username}") do
      {:ok, response} ->
        # Parse response here...
        {:ok, user}
      {:error, error} ->
        # Handle error here...
        {:error, reason}
    end
  end
end
```

Finally, in our test helper, we define a mock `MyApp.GithubAPI.Mock` for the API and set it in our application environment.

```elixir
# test/test_helper.exs
Mox.defmock(MyApp.GithubAPI.Mock, for: MyApp.GithubAPI)
Application.put_env(:my_app, GithubAPI, MyApp.GithubAPI.Mock)
```

Now we can refactor the test case to use `Mox` for mocking out the call to the external API:

```elixir
# test/my_app/github/api_test.exs
defmodule GithubAPITest do
  use ExUnit.Case, async: true

  import Mox

  setup :verify_on_exit!

  test "can fetch a user" do
    MyApp.GithubAPI.Mock
    |> expect(:fetch_user, fn "octocat" -> {:ok, %Github.User{login: "octocat"}} end)
    assert {:ok, %Github.User{login: "octocat"}} = GithubAPI.fetch_user("octocat")
  end
end
```

There are several interesting aspects to the above code. Let's break it down.

First, we use the `expect` function to define an expectation on the API. We expect a single call to `fetch_user` with the argument `"octocat"`, and we mock it to return a `{:ok, %Github.User{}}` tuple.

Now, when we call `GithubAPI.fetch_user/1` in the test, it will reach for that mocked expectation instead of the default implementation. Thus, we can safely assert the result of the call without making calls to the actual API.

The `verify_on_exit!` function as `setup` ensures that all expectations defined with `expect` are fulfilled when a test case finishes. So if we define an expectation on `fetch_user` and it isn't actually called (e.g., if the implementation changes later), we see the test fail.

Finally, notice that we don't need to mark the test as non-async here. That’s because Mox supports mocking inside async tests. So you can define two completely different mocks in two test cases, and they will both pass, even if they run simultaneously.

You can also define a global stub to avoid providing mocks in every test. This is useful if your client is being used in several places and you don’t want to explicitly define and validate expectations everywhere.

To do this, just update your ExUnit case template:

```elixir
defmodule MyApp.Case do
  use ExUnit.CaseTemplate

  setup _context do
    Mox.stub_with(MyApp.GithubAPI.Mock, MyApp.GithubAPI.Stub)
  end
end
```

And create a stub with some static results:

```elixir
defmodule MyApp.GithubAPI.Stub do
  @behaviour MyApp.GithubAPI

  @impl true
  def fetch_user("octocat"), do: {:ok, %Github.User{login: "octocat"}}
end
```

Now every time you `use MyApp.Case` in a test case, you don't need to manually mock calls to `GithubAPI` — they will automatically be forwarded to the stubbed module. This works well when you have calls to the `GithubAPI` that you will hit across several test suites. With a stub, you can be sure that such calls always return a specific and stable response without having to mock them out manually.

### Test the External Client

If you are following along closely, you will notice that we didn’t actually test the `MyApp.Github.HTTP` module at all.
Don’t worry, we won’t leave it untested.

But the recommended way here is to do integration tests instead of using mocking at that level.

We will also configure it so that these tests don’t run when you run the full test suite.

```elixir
# test/my_app/github/http_test.exs
defmodule MyApp.GithubAPI.HTTPTest do
  use ExUnit.Case, async: true

  # All tests will ping the API
  @moduletag :github_api

  # Write your tests here
end

# test/test_helper.exs
ExUnit.configure exclude: [:github_api]
```

The next step is to set up your CI pipeline to ensure that these tests run when required.

For example, you can configure it to run on all pull requests targeting `main` to avoid reaching the external API on every commit. You can also check that your CI pipeline runs before anything is merged to the main branch.

To do this, use the `include` command line flag when running `mix test`.

```elixir
mix test --include github_api
```

There are several pros to the above strategy. You are now testing all parts of the application, including the calls to the external API (this part is not exactly dependent on Mox, but since we mocked out a significant part of our HTTP client, we must test it separately). We can also define global stubs, specific mocks, and expectations only when required, which makes most of our test suite very clean and concise.

The major drawback is that it requires quite some setup — you need a new `behaviour` with `@callbacks` for each public method you want to mock out. You have to implement that `behaviour` inside your module and finally set up `Mox` to mock out that implementation during tests.

And since we are now reaching the external API, the tests can be flaky, depending on the API's availability.

## Mimic

If you are used to [Mocha](https://github.com/freerange/mocha) for other languages, you can check out [Mimic](https://github.com/edgurgel/mimic). It lets you define stubs and expectations during tests by keeping track of the stubbed module in an ETS table.

It also maintains separate mocks for each process, so you can continue using async tests. It’s a great alternative to [Mock](https://github.com/jjh42/mock) — but that also means the same caveat applies: be careful about what you mock.

Here’s how a sample test looks with Mimic:

```elixir
# test_helper.exs
Mmimc.copy(HTTPoison)
ExUnit.start()

# github_api_test.exs
defmodule GithubAPITest do
  use ExUnit.Case, async: true
  use Mimic

  test "can fetch a user" do
    url = "https://api.github.com/users/octocat"
    expect(HTTPoison, :get, fn ^url -> {:ok, %{status_code: 200, body: ~s({"login": "octocat"})}} end)
    assert {:ok, %Github.User{login: "octocat"}} = GithubAPI.fetch_user("octocat")
  end
end
```

`expect/3` automatically verifies that the function is called.
And all `expect` and `stub` calls can be [chained](https://github.com/edgurgel/mimic#using) to make up clear mocking code.

## Use Mocking Carefully for Your Elixir App

Mocking is an important and necessary part of any test suite.
But the thing to remember about mocking is that it is imperative to do it right.

For example, it might be tempting to mock out an internal API to quickly simulate _something_ for a test. And yes, it works for quick unit tests. But then, someone else (or maybe you) comes along a while later and changes that _something_ that you mocked earlier.

Your tests still pass since they use the mocked value. But in practice, the thing is now broken, and it will be caught much later in the feedback loop (or worse still, be shipped to the user as-is).

## Wrap Up

In this post, we explored several mocking strategies you can use in Elixir tests. The safest (and the one you should consider using first) is `Mox`, as it forces mocked modules to have a defined behavior. Mox can therefore catch issues that arise from API changes during compilation.

Reach out for `Mimic` or `Mock` when you need to mock external libraries you don't have any control over.

Until next time, happy mocking!

**P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, [subscribe to our Elixir Alchemy newsletter and never miss a single post](/elixir-alchemy)!**