Unit Testing HTTP Client error response

Hello all,

Let’s say we have a module / wrapper that carries out calls to an external HTTP service to fetch the body of the response, for example:

defmodule RandomThirdPartyClient do

  def perform_request(data) do
    url = "http://www.some-service?q=#{data}"

    case HTTPoison.get(url) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        {:ok, body}

      {:error, %HTTPoison.Error{reason: reason}} ->
        {:error, reason}
    end
  end
end

How would one test that the function returns an error (which would result from a HTTPoison.Error)? Would it be best to:

  • use the package Bypass to simulate lack of connectivity
  • Stubbing the function to return an error

Or something completely different? Or am I missing the point entirely here? I ask because excoveralls test coverage requirement.

Thanks.

p.s I currently use exVCR to test the happy path to avoid carrying out actual requests.

1 Like

+1 for using exVCR and I do think this is the happiest path possible to have some predictability in the test cases.

Yes, I don’t have an issue with the happy path but the unhappy/negative path. As the URL is static and used within the function itself, I’m unclear how to receive a HTTPoison error. Only idea is the stubbing part maybe.

Have you tried recording custom cassettes so you can test both cases?
There’s a section in the README that talks about it: https://github.com/parroty/exvcr#custom-cassettes.

PS.: I haven’t tried it yet, but I think you can match the cassettes against the status code of the request or something else (whatever makes the most sense for your use case): https://github.com/parroty/exvcr#matching-options.

Thanks for the reply.

I learnt something new reading that!

However, I did use the exVCR stub functionality and creating custom cassettes. However, I need to test on the {:error, _} match from the HTTPoison.get response not the details of the response itself.

I guess the above methods I originally mentioned are the only ways unless I change the interface for the API, ie just returning the HTTPoison.response instead of the html

Yes, I think I understand your problem now… Given that you only want to test for the failing case, which represents an invalid request, I don’t think ExVCR can help you here.

Perhaps the simplest way would be just providing an invalid URL like:

HTTPoison.get("http://www.invalid.invalid")
{:error, %HTTPoison.Error{id: nil, reason: :nxdomain}}

But this won’t help you very much if you want to test for specific failure scenarios or if you care for specific error results.

+1 for Bypass - it makes this pretty straightforward. There is a good example in the docs.

1 Like

Mock is a simple library that allows you to test different methods responses by mocking the module. Here what it could look like to your example:

defmodule ShowMockLibraryTest do
  use ExUnit.Case

  alias HTTPoison
  import Mock

  defmodule Client do
    def perform_request(query) do
      url = "http://www.some-service?q=#{query}"

      case HTTPoison.get(url) do
        {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
          {:ok, body}

        {:error, %HTTPoison.Error{reason: reason}} ->
          {:error, reason}
      end
    end
  end

  @ok_response %HTTPoison.Response{
    status_code: 200,
    body: "Some content"
  }

  @error_response %HTTPoison.Error{reason: "Some reason"}

  test "should return the body if request returns 200" do
    with_mock HTTPoison, get: fn _ -> {:ok, @ok_response} end do
      assert {:ok, "Some content"} = Client.perform_request("some-query")
    end
  end

  test "should return the reason if request returns an error" do
    with_mock HTTPoison, get: fn _ -> {:error, @error_response} end do
      assert {:error, "Some reason"} = Client.perform_request("some-query")
    end
  end

  test "should add the query to the url before calling it" do
    with_mock HTTPoison, get: fn _ -> {:ok, @ok_response} end do
      assert {:ok, _body} = Client.perform_request("some-query")
      assert_called(HTTPoison.get("http://www.some-service?q=some-query"))
    end
  end

  test "should raise if any other response is received" do
    with_mock HTTPoison, get: fn _ -> {:error, "Unknown Error"} end do
      assert_raise CaseClauseError, fn ->
        Client.perform_request("some-query")
      end
    end
  end
end


If you use Tesla as your client, you can use their built-in mocks. See Tesla.Mock.

Another possible approach is to separate concerns in differents methods. You basically isolate code you can’t control from code that you can. This is what I mean:

defmodule SeparateConcernsTest do
  use ExUnit.Case

  alias HTTPoison

  defmodule Client do
    def perform_request(query) do
      query
      |> url()
      |> HTTPoison.get()
      |> handle()
    end

    def url(query), do: "http://www.some-service?q=#{query}"

    def handle({:ok, %HTTPoison.Response{status_code: 200, body: body}}) do
      {:ok, body}
    end

    def handle({:ok, %HTTPoison.Error{reason: reason}}), do: {:error, reason}
  end

  @ok_response %HTTPoison.Response{
    status_code: 200,
    body: "Some content"
  }

  @error_response %HTTPoison.Error{reason: "Some reason"}

  test "should return the body if request returns 200" do
    assert {:ok, "Some content"} = Client.handle({:ok, @ok_response})
  end

  test "should return the reason if request returns an error" do
    assert {:error, "Some reason"} = Client.handle({:error, @error_response})
  end

  test "should add the query to the url" do
    assert Client.url("some-query") == "http://www.some-service?q=some-query"
  end

  test "should raise if any other response is received" do
    assert_raise CaseClauseError, fn ->
      Client.handle({:error, "Unknown Error"})
    end
  end
end
1 Like

+1 for Bypass
-10 for any kind of mocking in this case

Bypass allows you simulate a real interaction at the TCP/HTTP level with a fake server. That’s what I personally need in order to have piece of mind when testing a client library for a remote service. A few advantages:

  • You’re testing the nitty-gritty details of your HTTP request. Consider for example authentication, content encoding/decoding, special headers that may be required etc. You can simulate all of this using plugs in Bypass, ignoring all the details of whatever HTTP client library you’re using (your remote service doesn’t know about it, either)
  • As a consequence, if you later on decide to swap your HTTP client library with something else, your tests don’t need to change
2 Likes