Mock HTTP Calls in Elixir Tests


Here's a way to mock HTTP requests in Elixir ExUnit test.

Install library

There's a nice library, mox, which we can install via mix.exs deps:

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

Mock in test environment

In our app, we use the httpoison library for doing web requests.

In our test environment, we don't want those requests to really be sent. So, we're going to set up a mock version of the HTTPoison module, using Mox. In test_setup.exs, add this line:

Mox.defmock(MockHTTPoison, for: HTTPoison.Base)

Configure application to use mock

We have a mock defined. Most of the time, like in dev and prod, we don't want to use it. We want to use the regular HTTPoison. Let's set up an http_client in config/config.exs:

config :myproj, 
  http_client: HTTPoison

Then in config/test.exs, we configure the application's http_client config to be the mock:

config :myproj, 
  http_client: MockHTTPoison

Use the client in source

Now that we've setup config for the http client, we have to use that configured client in source. Something like this in lib/my_src.ex:

defmodule MySrc do
  @http_client Application.compile_env(:myproj, :http_client, HTTPoison)

  def do_stuff() do
    res = @http_client.post("#{some_host}/api", %{"body" => "stuff"}, [{"Content-Type", "application/json"}])
    # ...
  end
end

Because we're using the configured http_client, this will never really make a network request when we run this code in test.

Expect a request

Now we need to tell our test what we expect to happen. Mocks have now opened us up to asserting implementation details (aka white box testing). That's not great, but it's what we started out to do, so let's finish it. We need to say when we expect @http_client.post to be called, and stub out what it will return in test.

Any expectations that a mock will be called need to be done before we run our subject under test code. Then run our code. Then do assertions, as usual. And now, a part of these assertions will now be a verification that all our mock expectations were met. We can do this automatically at the end of all tests in the suite, by calling a setup function. In my_test.exs, let's put a full test in place:

defmodule MyTest do
  setup :verify_on_exit!

  test "http stuff happens" do
    expect(MockHTTPoison, :post, fn _url, _body, _headers -> 
      {:ok, 
       %HTTPoison.Response{
         status_code: 201,
         body: Jason.encode!(%{
           data: "from the server"
         }),
         headers: [{"content-type", "application/json"}]
       }}
    end)

    assert(MySrc.do_stuff() == "expected value")
  end
end

What to notice? We're verifying mocks after every test exit. Before we run MySrc.do_stuff, we're expect()ing what the mock http client will do. We're saying that the client post function will be called. The function passed in has the params for the call to post. We can make assertions on them too, if we'd like (not shown here). I usually like to keep those light, attempting to tie the test to only the minimum of implementation details. Then we have otherwise-normal test case assertions.

Expect no request

Sometimes we want to make sure that the http request is not attempted. The test pieces are similar. We're still using the mock. But we're only going to stub the behavior for the post call. What should happen if the mock is called? Fail the test.

  test "http stuff does not happen" do
    stub(MockHTTPoison, :post, fn _url, _body, _headers -> 
      flunk("no http requests")
    end)

    assert(MySrc.do_stuff() == "expected value")
  end

mix test that, avoid the network and cry about our white box testing.