Elixir Patterns for Testing with Mox

When I need mocks in my Elixir tests, I prefer to use the Mox library. Mox follows the principle of mocking-by-explicit contract. The mox readme explains it like this:

  1. No ad-hoc mocks. You can only create mocks based on behaviours
  2. No dynamic generation of modules during tests. Mocks are preferably defined in your test_helper.exs or in a setup_all block and not per test
  3. Concurrency support. Tests using the same mock can still use async: true
  4. Rely on pattern matching and function clauses for asserting on the input instead of complex expectation rules

I've found that this works very well with the overall design of the Elixir language. Elixir developers generally prefer explicitly combining functional pieces over any sort of magical hidden state. Some of the other mocking libraries fiddle with your function definitions behind the scenes. I've found that this makes test code harder to read and debug.

When you are using Mox, I believe that it helps if you follow certain patterns. I've worked on several projects using Mox at this point, and I've seen the pain that can come from not establishing those pattern up front. I will summarize my recommendations briefly here before showing some examples:

  1. You should define one facade module which both your application and test code call in almost all cases.
  2. The facade module should delegate all its work to either the true adapter or the mock adapter, depending on the environment.
  3. One piece of configuration should control which adapter is used, and that configuration should be wrapped up in a nice easy to call function.

To boil that down to only one sentence: your application code should not know or care that you are using Mox. If you find your application code caring that it could be mocked out in a test later, you need a new set of abstractions. In fact, most of your test code shouldn't need to know about the mocks, except where it needs to set function call expectations.

What does the code to do this look like? Let's say your application uses a weather API and you don't want to use the real API in your test suite.

Let's say this is the module your application already has for communicating with the weather API:

1defmodule Weather do
2 def current_weather(zip_code) do
3 # makes a GET request to the API to ask for the current weather in a zip code
4 end

We would define a behaviour for a weather adapter:

1defmodule WeatherBehaviour do
2 @callback current_weather(binary) :: map

We would have the Weather module adopt the behavior and we would rename it so that it can serve as our live api adapter. Later, we'll tell our application to use the LiveWeather adapter by default.

1defmodule LiveWeather
2 @behaviour WeatherBehaviour
4 def current_weather(zip_code) do
5 # makes a GET request to the API to ask for the current weather in a zip code
6 end

At this point, we need to create a new Weather module so that all of our application code doesn't explode. Our new Weather module won't do much. It will just delegate down to either LiveWeather, or the mock weather adapter as appropriate for the environment:

1defmodule Weather do
2 defdelegate current_weather(zip), to: WeatherApp.weather_adapter()

and we'll need to define weather_adapter/0 in our application class:

1defmodule WeatherApp do
2 def weather_adapter do
3 Application.get_env(:weather_app, :weather_api_adapter, LiveWeather)
4 end

By default, the application will use LiveWeather. This is good for development and production. However, in the test environment, we need to tell the system to use the mock. In config/test.exs:

1config :weather_app, weather_api_adapter: WeatherMock

And we need to actually define our mock! In test/test_helper.exs:

1Mox.defmock(WeatherMock, for: WeatherBehaviour)

That's all the setup. Notice that nothing outside of the Weather module and it's configuration changed. None of our controllers or other contexts were disturbed in the process.

If we want to write a test for a function that uses our Weather API, we need to tell Mox what function calls to expect and what to return:

1defmodule UserTest do
2 # standard test boilerplate as before
4 test "current_weather/1 gives the current weather for the user" do
5 user = %User{zip_code: "19120"}
7 WeatherMock
8 |> expect(:current_weather, fn "19120" ->
9 # I've heard it is always sunny there
10 %{"description" => "clear"}
11 end)
13 assert %{"description" => "clear"} = User.current_weather(user)
14 end