Bounti

Bounti

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.

Showing Posts 8 to 1

trisolaran

trisolaran

+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
gpereira

gpereira

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
srowley

srowley

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

thiagomajesk

thiagomajesk

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.

Bounti

Bounti OP

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

thiagomajesk

thiagomajesk

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.

Bounti

Bounti OP

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.

thiagomajesk

thiagomajesk

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

— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
ryanwinchester
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted” Version...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews