Elixir and Mox

The blog post is a fast walk through on on how to setup Mox on Elixir and to start using mocking in your test suites. The community has many articles online for you to try out.

Setup

I really enjoyed the article on Mocks and explicit contracts. It explains the core idea around mocking in elixir and has a good walk through. I'm going to use it as the case study for this blog post.

Scenario

You are going to have a Twitter API that will be used inside a controller for your phoenix application. This Twitter API needs to be mocked in order to prevent it from making 3rd party api calls during tests.

Setup

I would read up on the instructions from the GitHub repo. But if you are running the latest of elixir it should be as simple as.

def deps do
[
{:mox, "~> 1.0", only: :test}
]
end

Create Behavior / Contract

Behaviours within Elixir are similar to interfaces in other OOP languages. The documentation has a write up and examples on how to think of them.

Our contract will state that in order to get a tweet we need to pass in a user_id that is a string and return a tuple(). In the future you can add more methods but for now keep it simple.

# lib/api/ap_behaviour.ex
defmodule MyApp.Api.ApiBehaviour do
@callback get_tweet(user_id :: String.t()) :: tuple() # [!code highlight]
end

InMemory - Demo Implementation

In order to use the contract you need to place @behaviour MyApp.Api.ApiBehaviour within your MyApp.Api.InMemory and then @impl MyApp.Api.ApiBehaviour just above the actual implementation of the callback. If you don't do this Elixir will complain to you.

The MyApp.Api.InMemory implementation can be used for testing and for development.

Within the get_tweet/1 it's loading json of the actual response that I downloaded and saved into my test directory. This let's me also parse the information.

# lib/api/in_memory.ex
defmodule MyApp.Api.InMemory do
@behaviour MyApp.Api.ApiBehaviour
@moduledoc """
This module is for stubbing out the MyApp.Api module
Use for tests and development
"""
@impl MyApp.Api.ApiBehaviour
def get_tweet(_user_id) do
"test/fixtures/tweet.json"
|> File.read!()
# I created a module for parsing JSON into a tuple
|> MyApp.Api.TweetResponse.parse_tweet_from_json()
end
end

Client - Live Implementation

This is where you put the real implementation, where you need to deal with HTTP responses and the parsing of those responses.

# lib/api/client.ex
defmodule MyApp.Api.Client do
@behaviour MyApp.Api.ApiBehaviour
@moduledoc """
This module is the live implementation
"""
@impl MyApp.Api.ApiBehaviour
def get_tweet(user_id) do
# Make HTTP request
# get the response
# parse the response into a tuple
end
end

Create Context / Bound

This is key. We are going to create the context or bound that will pull in the correct implementation depending on the environment we are in.

This is the file we are going to use in our controller and it's implementation will change depending on the environment that the application is running in.

defmodule MyApp.Api do
@moduledoc """
This is the file we use to interact with Twitter
"""
def get_tweet(user_id), do: api_implementation().get_tweet(user_id)
defp api_implementation() do
Application.get_env(:my_app, :api) ||
raise "Missing configuration for Api. Do you want MyApp.Api.InMemory or MyApp.Api.Client"
end
end

Configuring the environment with the implementation

We need to define in our environment which implementation do we want to be using. This is key. In this example we are going to use the MyApp.Api.Client for production and then for development and testing the MyApp.Api.InMemory version.

# config/prod.exs
config :my_app, :api, MyApp.Api.Client
# config/dev.exs
config :my_app, :api, MyApp.Api.InMemory
# config/test.exs
config :my_app, :api, MyApp.Api.InMemory

Test Helpers

Make this change within your test helpers. Take note that we are calling our mock module ApiMock. Mox is autogenerated this module name for use and it will be available within our tests. It's just a shell module.

# test/test_helper.exs
Mox.defmock(ApiMock, for: MyApp.Api.ApiBehaviour) # [!code ++]
Application.put_env(:my_app, :api, ApiMock) # [!code ++]
ExUnit.start()

Test with Expect

At this point you can write your tests with Mox.expect and write the response inline as shown below.

# test/api_test.exs
defmodule MyApp.ApiTest do
use ExUnit.Case
import Mox
setup :verify_on_exit!
describe "get_tweet/1" do
test "fetches tweet" do
expect(ApiMock, :get_tweet, fn args ->
assert args == "jack"
# here we decide what the mock returns
# if we decide to use the expect
{:ok, %{twitter: "some response here"}}
end)
assert {:ok, response} = MyApp.Api.get_tweet("jack")
assert response.twitter == "some response here" # you assert that certain values exist
end
end
end

Test with a stubbing an entire module

You can also stub out the entire module with the MyApp.Api.InMemory module. This module already contains all the JSON responses you would get from twitter so you can test this here. The setup will be slightly different as you will use Mox.stub_with/2 within setup.

# test/api_test.exs
defmodule MyApp.ApiTest do
use ExUnit.Case
# Mox will use the implementation of MyApp.Api.InMemory
# for this test. The test will look pretty straight forward
setup do
Mox.stub_with(ApiMock, MyApp.Api.InMemory)
:ok
end
describe "get_tweet/1" do
test "fetches tweet" do
assert {:ok, response} = MyApp.Api.get_tweet("jack")
assert response.twitter # you assert that certain values exist
end
end
end

Test with a live implementation

You can then create another file and do an integration test where you are testing the live implementation. When doing this I would follow the recommendation from the article Mocks and explicit contracts where you should tag these test and avoid running with your other tests.

So you just write the following

# test/my_app_test.exs
defmodule MyApp.ApiTest do
use ExUnit.Case
# All tests will ping the twitter API
@moduletag :twitter_api
# Mox will use the implementation of MyApp.Api.Client
setup do
Mox.stub_with(ApiMock, MyApp.Api.Client)
:ok
end
describe "get_tweet/1" do
test "fetches tweet" do
assert {:ok, response} = MyApp.Api.get_tweet("jack")
assert response.twitter # you assert that certain values exist
end
end
end
# test/test_helper.exs
ExUnit.configure exclude: [:twitter_api]

But you can still run the whole suite including the tests tagged :twitter_api when you want them:

mix test --include twitter_api

Or run only the tagged tests:

mix test --only twitter_api

Testing controllers

So at this point we have mocked and testing our module. Now what happens when we use that module within a controller. How do we deal with that.

# test/my_app_web/controllers/api/tweet_controller_test.exs
defmodule MyAppWeb.Api.TweetController do
use MyAppWeb, :controller
action_fallback MyAppWeb.Api.FallbackController
def show(conn, params) do
with {:ok, result} <- MyApp.Api.get_tweet(params["user_id"]) do
render(conn, "show.json", user: result)
end
end
end

MyApp.Api.InMemory Module

If you were to run this controller in development mode you could navigate to that page and would be getting the data response from your MyApp.Api.InMemory module. This would allow you to start playing with the presentation of the page and figure out how to present stuff.

Test controller with expect

This is a controller test that has a module that we have already tested out earlier. Controller tests should validate very simple branching logic.

# test/my_app_web/controllers/api/tweet_controller_test.exs
defmodule MyAppWeb.Api.TweetControllerTest do
use MyAppWeb.ConnCase
import Mox
@api_mock ApiMock
setup :verify_on_exit!
describe "GET tweet/:user_id" do
test "success, return a user", %{conn: conn} do
expect(@api_mock, :get_tweet, fn args ->
# assert argument passed in
assert args == "jack"
# fake response
{:ok, %{user: "jack"}}
end)
conn = get(conn, "/api/jack")
assert conn.params["user_id"] == "jack"
assert {:ok, _} == json_response(conn, 200)
end
test "error, returns error for unknown user", %{conn: conn} do
expect(@api_mock, :get_tweet, fn args ->
# assert argument passed in
assert args == "00000000"
# fake response
{:error, :not_found}
end)
conn = get(conn, "/api/00000000")
assert conn.params["user_id"] == "00000000"
assert {:error, _} == json_response(conn, 200)
end
end
end

Test controller with stubbing the entire module

In this controller test we are stubbing out the entire module. As we have done in earlier example.

# test/my_app_web/controllers/api/tweet_controller_test.exs
defmodule MyAppWeb.Api.TweetControllerTest do
use MyAppWeb.ConnCase
setup do
Mox.stub_with(ApiMock, MyApp.Api.InMemory)
:ok
end
describe "GET tweet/:user_id" do
test "success, return a user", %{conn: conn} do
conn = get(conn, "/api/jack")
assert conn.params["user_id"] == "jack"
assert {:ok, _} == json_response(conn, 200)
end
test "error, returns error for unknown user", %{conn: conn} do
conn = get(conn, "/api/00000000")
assert conn.params["user_id"] == "00000000"
assert {:error, _} == json_response(conn, 200)
end
end
end

Reference materials

This is only the beginning, but mocking with Mox and using this behaviour / contract approach makes testing very predictable. The terminology around mock, double, and stub trips a lot of people up — the distinction that matters here is that a mock verifies the calls it receives,1 while a stub just answers with predefined data.2

If you want to go deeper, the article that this walkthrough is built on3 is the single best starting point, and the links below4 fill in the corners.

  1. Mock — a module that registers the calls it receives, letting you verify that the right params were passed in and the expected actions were performed.

  2. Stub — a module that holds predefined data and uses it to answer calls during tests, without any verification.

  3. José Valim, Mocks and Explicit Contracts — the post that popularised the "mock as a noun, not a verb" contract approach used throughout this walkthrough.