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.