Elixir
Mocking API’s with Elixir
Elixir allows me to create fast, scalable applications and be productive like never before. Almost every time I think I need a tool, I discover a simple and easy way to do the same thing with just vanilla Elixir.
For example, coming from the Ruby world, I instinctively want to reach for a gem to get things done. When testing I almost always reach for mock5, which allows me to easily mock APIs with Sinatra’s elegant DSL. In the Elixir world, such tooling doesn’t exist yet – but I don’t think it’s even necessary.
Let’s imagine we are writing a tool that reaches out to Github’s api and gives us information about users. Here is what the request and response might look like:
$ curl -X "GET" "https://api.github.com/users/mwoods79" \
-H "Accept: application/vnd.github.v3+json"
{
"login": "mwoods79",
"avatar_url": "https://avatars.githubusercontent.com/u/129749?v=3",
# Lots of other stuff
}
Now that we know what the response looks like, we can write a test. Our test is going to assert on a function get/1
. (This test is fictitious and doesn’t really do anything; it's just an exercise to show how easy it is to mock an API.)
defmodule Example.UserTest do
use ExUnit.Case
alias Example.User
test "retrieving user" do
assert {:ok, response} = User.get("mwoods79")
assert %{"login" => "mwoods79"} = response.body
assert %{"avatar_url" => _} = response.body
end
end
Now that we have a failing test, I am going to make an abstraction around the Github API. This is going to be a base that can be used with other modules. You will see shortly that this will be the piece we replace during tests.
defmodule Example.Github do
use HTTPoison.Base
def headers do
%{
"Content-type" => "application/json",
"Accept" => " application/vnd.github.v3+json"
}
end
def make_request(:get, url) do
get(url, headers)
end
# Example of a POST
# def make_request(:post, url, body) do
# post!(url, body, headers)
# end
# HTTPosion Hooks
def process_url("/" <> path), do: process_url(path)
def process_url(path), do: "https://api.github.com/" <> path
def process_response_body(body) do
body |> Poison.decode!
end
end
defmodule Example.User do
import Example.Github, only: [make_request: 2]
def get(username) when is_binary(username) do
make_request(:get, "/users/#{username}")
end
end
Now if we run the tests, they pass. Which is great, except that we're actually hitting the live Github API. This will slow our tests down, and we're dealing with production data. Luckily, Elixir and mix have our back. Uncomment the following from your config/config.exs
:
import_config "#{Mix.env}.exs"
Add this to a new file named config/test.exs
:
use Mix.Config
config :example, :github_api, Example.GithubMock
Create config/dev.exs
and config/prod.exs
and add the following:
use Mix.Config
config :example, :github_api, Example.Github
Now modify our Example.User
module to use our changes:
defmodule Example.User do
@github_api Application.get_env(:example, :github_api)
def get(username) when is_binary(username) do
@github_api.make_request(:get, "/users/#{username}")
end
end
That’s it! We're ready to create a mock:
defmodule Example.GithubMock do
def make_request(:get, "/users/mwoods79") do
{:ok, %{
body:
%{
"login" => "mwoods79",
"avatar_url" => "https://avatars.githubusercontent.com/u/129749?v=3",
}
}
}
end
end
Run the tests again, and you see they still pass. And if you open up a mix REPL, you can hit real data. Go ahead try it.
That’s it! There's no new DSL to learn and no extra dependency in your app – just Elixir and productivity.