Heading image for post: Mocking Requests in Elixir Tests

Elixir

Mocking Requests in Elixir Tests

Profile picture of Jason Cummings

When writing code that relies heavily on the results of external API calls, we need to be thinking about how we're going to test how our program reacts to different responses. Making an actual request would make our test suite extremely slow, so this is a good time to mock our responses.

Here's where we're at: we're working though an OAuth 2.0 Authorization flow. Without going into too much detail, the first step is to request an authorization code from the API by sending over encoded credentials. The next step, and the one we're using for this example, is the user authentication. After successful authorization the external API will hit our authentication endpoint with the authorization code in the params. That's all we really need to know here.

defmodule Authentication do
  def authenticate(conn, %{"code" => code}) do
    if get_refresh_cookie(conn) do
      AuthenticationClient.post(conn, refresh_body_params(conn))
    else
      AuthenticationClient.post(conn, body_params(code))
    end
  end

  # ... functions and stuff
end

defmodule AuthenticationClient do
  @url "http://www.an-awesome-api.com/authenticate"

  def post(conn, params) do
    case HTTPoison.post(@url, params, AnAwesomeApi.headers) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        { :ok, response } = Poison.decode(body)
        cookies = get_cookies_from_response(response)
        conn = set_cookies(conn, cookies)
        { :ok, conn }
      {:error, %HTTPoison.Error{reason: reason}} ->
        { :error, reason, conn }
    end
  end
end

defmodule OauthAuthorizationFlowTest do
  use ExUnit.Case
  use Plug.Test

  describe "authentication" do
    test "a successful attempt sets the cookies" do
      conn = conn(:post, "/authenticate", %{"code" => "valid"})
        |> Plug.Conn.fetch_cookies

      assert { :ok, new_conn } = Authentication.authenticate(conn, conn.params)
      assert new_conn.cookies["spotify_access_token"]  == "access_token"
      assert new_conn.cookies["spotify_refresh_token"] == "refresh_token"
    end
  end
end  

Our Authentication module uses the AuthenticationClient to make a POST request for authentication. In the above test, we will end up hitting our client and making an actual http call. This is not what we want. In order to mock this request, we need to pull out our http call into its own function.

defmodule AuthenticationClient do

  def post(conn, params) do
    case AuthRequest.post(params) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        { :ok, response } = Poison.decode(body)
        cookies = get_cookies_from_response(response)
        conn = set_cookies(conn, cookies)
        { :ok, conn }
      {:error, %HTTPoison.Error{reason: reason}} ->
        { :error, reason, conn }
    end
  end
end

defmodule AuthRequest do
  @url "https://www.an-awesome-api.com/api/token"

  def post(params) do
    HTTPoison.post(@url, params, AnAwesomeApi.headers)
  end
end

Not only did we pull it into it's own function, we pulled it into it's own module. Why?

We will be using Mock, a wrapper for the Erlang mocking library Meck. When Mock stubs a function, it creates it's own version of the module containing that function with the stubbed function available, and nothing else. If we moved the call to HTTPoison to another function within the same module, post/2 would be undefined.

Let's tell the test to use a mock api client: The with_mock macro is documented in the library, linked above.

defmodule OauthAuthorizationFlow do
  use ExUnit.Case
  use Plug.Test
  import Mock

  describe "authentication" do
    test "a successful attemp sets the cookies" do
      with_mock AuthRequest, [post: fn(params) -> AuthenticationClientMock.post(params) end] do
        conn = conn(:post, "/authenticate", %{"code" => "valid"})
          |> Plug.Conn.fetch_cookies

        assert { :ok, new_conn } = Authentication.authenticate(conn, conn.params)
        assert new_conn.cookies["access_token"]  == "a_valid_access_token"
        assert new_conn.cookies["refresh_token"] == "a_valid_refresh_token"
      end
    end
  end
end  

Now, our call to AuthRequest.post/1 will be sent to AuthenticationClientMock.post/1, which doesn't exist yet. We can throw a debugger into our actual call using a catch all at the top of our case statement to get a copy/pastable response to use in our mock. (If we did things in this exact order we'd also need to comment out the with mock line so we can hit the actual request.)

defmodule AuthenticationClient do

  def post(conn, params) do
    case AuthRequest.post(params) do
      {:ok, response } -> require IEx; IEx.pry
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        { :ok, response } = Poison.decode(body)
        cookies = get_cookies_from_response(response)
        conn = set_cookies(conn, cookies)
        { :ok, conn }
      {:error, %HTTPoison.Error{reason: reason}} ->
        { :error, reason, conn }
    end
  end
end

response will be the struct returned by HTTPoison. We'll paste it into our mock module (and replace some lengthly values with "foo") in successful_response/0, then remove our catchall clause and debugger.

#test/mocks/authentication_client_mock.exs
defmodule HTTPoison.Response do
  defstruct body: nil, headers: nil, status_code: nil
end

defmodule AuthenticationClientMock do
  def post(params) do
    { :ok, successful_response }
  end

  defp successful_response do
    %HTTPoison.Response{
      body: "{\"access_token\":\"a_valid_access_token\",\"token_type\":\"Bearer\",
        \"expires_in\":3600,\"a_valid_refresh_token\":\"refresh_token\"}",
      headers: [{"Server", "nginx"}, {"Date", "Thu, 21 Jul 2016 16:52:38 GMT"},
        {"Content-Type", "application/json"}, {"Content-Length", "397"},
        {"Connection", "keep-alive"}, {"Keep-Alive", "timeout=10"},
        {"Vary", "Accept-Encoding"}, {"Vary", "Accept-Encoding"},
        {"X-UA-Compatible", "IE=edge"}, {"X-Frame-Options", "deny"},
        {"Content-Security-Policy",
          "default-src 'self'; script-src 'self' foo"},
        {"X-Content-Security-Policy",
          "default-src 'self'; script-src 'self' foo"},
        {"Cache-Control", "no-cache, no-store, must-revalidate"},
        {"Pragma", "no-cache"}, {"X-Content-Type-Options", "nosniff"},
        {"Strict-Transport-Security", "max-age=31536000;"}],
      status_code: 200
    }
  end
end

With our mock in place it doesn't matter if we use a real response or a mock response, our client post function will still pattern match on the same struct fields and behave the same. There would be more work if we wanted to mock out failed responses and other outcomes, but you get the idea.

Let's take another look at the line that sets up our mock:

with_mock AuthRequest, [post: fn(params) -> AuthenticationClientMock.post(params) end] do

There's a case for cleaning up this line of code. Since we've kept the involved modules small there likely won't be any different mock setups. That and the fact that we're going to need to use the same setup for more test cases, this is most likely a good candidate for being wrapped up in a macro.

I say probably because it's very easy to find yourself writing macros in any language that allows you to do so; however if anything can be done with simple functions, we should refrain from writing a macro. This is a good candidate for a macro in my opinion, so we'll go with it.

Now we can clean up our mocks like so:

defmodule OauthAuthorizationFlow do
  use ExUnit.Case
  use Plug.Test
  import Mock

  defmacro with_auth_mock(block) do
    quote do
      with_mock AuthRequest, [post: fn(params) -> AuthenticationClientMock.post(params) end] do
        unquote(block)
      end
    end
  end

  describe "authentication" do
    test "a successful attempt sets the cookies" do
      with_auth_mock do
        conn = conn(:post, "/authenticate", %{"code" => "valid"})
          |> Plug.Conn.fetch_cookies

        assert { :ok, new_conn } = Authentication.authenticate(conn, conn.params)
        assert new_conn.cookies["aaa_access_token"]  == "access_token"
        assert new_conn.cookies["aaa_refresh_token"] == "refresh_token"
      end
    end
  end
end  

If you want to see real world application of this, I took this from concept from a Spotify wrapper I'm working on.

If you're interested in your next project being written in Elixir, we're going nuts over Elixir and Phoenix at Hashrocket and we want to work with you. Get in touch today!

More posts about Elixir testing

  • Adobe logo
  • Barnes and noble logo
  • Aetna logo
  • Vanderbilt university logo
  • Ericsson logo

We're proud to have launched hundreds of products for clients such as LensRentals.com, Engine Yard, Verisign, ParkWhiz, and Regions Bank, to name a few.

Let's talk about your project