Need help with writing tests for an api client

I’ve written an api client for some GitHub operations I need to perform and I just can’t seem to wrap my head around how to test this thing. I’ve read Mock and Explicit Contracts and I’ve looked at Stubr but I’m not putting the pieces together for my own needs.

I might not be understanding correctly, but it seems like if I use the technique discussed in José Valim’s blog post, I’ll end up writing three modules, two of which will only be for dev and testing. So I’ll clutter up my code base with stuff that isn’t actually used by my app. And if I write a module that always returns the same response for testing, then how would I test for cases where the api might return different results? Finally, if I do that wouldn’t it be fair to say that I’m not testing my api client that would be used in production, but rather I’m testing the mock?

Here is a stripped down sample. The module has a function called health that will return true or false based on whether the auth token is returning a valid user.

defmodule GitHub.API do
  use GenServer

  def health(opts \\ []) do
    GenServer.call(__MODULE__, {:health, opts})
  end

  def start_link do
    GenServer.start_link(__MODULE__, Application.get_env(:siem_alerts, :github_token), name: __MODULE__)
  end

  def init(token) do
    {:ok, token}
  end

  def handle_call({:health, opts}, _from, state) do
    {:ok, response} = HTTPoison.get("https://api.github.com/user", [{"Authorization", "token #{state}"}])
    {:ok, body} = Poison.decode(response.body)
    health_status = case body do
      %{"type" => "User"} -> true
      _ -> false
    end

    case Keyword.get(opts, :details, false) do
      true -> {:reply, body, state}
      false -> {:reply, health_status, state}
    end
  end

end

Assume that the above is working properly. My phoenix controller just returns a json status when you GET /health that consists of running the health function above. How could I make these two tests pass?

defmodule MyApp.HealthControllerTest do
  use MyApp.ConnCase

  describe "getting health status" do
    test "GET /health indicates if GitHub key is valid" do      
      response = build_conn
      |> get(health_path(build_conn, :index))
      |> json_response(200)

      assert response == %{"healthy" => true, "valid_github_key" => true}
    end

    test "GET /health indicates if GitHub key is not valid" do
      response = build_conn
      |> get(health_path(build_conn, :index))
      |> json_response(200)

      assert response == %{"healthy" => false, "valid_github_key" => false}
    end
  end
end
1 Like

I’ve used an HTTP recorder (ExVCR) to test a library that accesses an external API. It allows you to make real HTTP requests, capture the response to disk, and then you can replay from these fixtures in subsequent test runs. That would allow you to verify behaviour and you can adjust the response in the fixture files to simulate errors (e.g. HTTP response codes).

I’ve written an article about this approach to testing: HTTP unit tests using ExVCR.

3 Likes

It depends on where you draw a line between your app and the outside world, for API heavy stuff I use bypass (https://github.com/PSPDFKit-labs/bypass) that lets you have “an almost real” http server that serves prebaked responses, you don’t need adapters to use it - your app just accesses the given localhost URL.

In any case you’d never test an external API, it’s about the reaction of your app to it. Prebaked responses are good unless you’re ok with having slow or inconsistent tests.

You can also mark some tests as “live” or something and access live API with these, but exclude them from normal suite.

2 Likes