Hashrocket.com / blog

Large elixir mocking

Mocking API’s with Elixir

posted on and written by Micah Woods in

Image 100x100 micah woods

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.

Posted in Elixir and tagged with testing, mocks