Mocks and Elixir

Walk through example of testing in elixir using Mox. This examples walks through testing a module that makes API calls to a 3rd party client. It's a long tutorial.

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()
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) # <- Add this
Application.put_env(:my_app, :api, ApiMock) # <- Add this

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 with the tests tagged :twitter_api if desired:

mix test --include twitter_api

Or run only the tagged tests:

mix test --include 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 stuff with Mox and using this behaviour / contract approach makes testing very predictable. I do find the terminology confusing around mock, double, stubs. But I’ll leave my interpretation of terms and some links to excellent blog posts and books.

Terminology

Mocks - Modules that register calls they receive. We can verify that params are passed in and expected actions were performed

Stub - Modules that hold predefined data and use it to answer calls during tests

Articles / Links

Mocks and Explicit Contracts
Elixir Mox Introduction
Elixir and Mox
Mocking Behaviour in Child Process
Revert Mocked Module
TDD with Phoenix