Stub all HTTP calls by default in Elixir (with Mimic)

Recreating the common Ruby practice of blocking external requests by default

originally published Fri Nov 10 2023 00:00:00 GMT+0000 (Coordinated Universal Time)

When I came to Elixir by way of Ruby, something that stuck out to me was mocking and stubbing HTTP calls. In Ruby, everyone just uses the standard library for HTTP, and everyone uses webmock or vcr , and when they use these packages, they set them up to stub all un-explicitly mocked calls. It's all simple, there's few choices to make.

In Elixir, the HTTP ecosystem is, unfortunately, not quite so standardized. There’s about a dozen different libraries that all see some amount of use, and a good amount of mocking tools too.

A thorough accounting of your different options in this issue is, unfortunately, beyond the scope of this post. But I will recommend this: Just use req for HTTP calls and mimic for mocking.

req is fantastic. It has a very modern-feeling API. It is, in my opinion, suitable for use both in an application and a library. It’s use in applications feels great: as simple as something like Ruby. In libraries, it also offers some great features.

mimic , in my opinion, blows most other mocking tools in Elixir out of the water. The API just ‘makes sense’ to me. If you haven’t tried it, I really recommend it.

💡

I suggest you just stop using mox . Sorry, I’ve read the blog post, I’ve tried to make it work, and I remain incredibly unconvinced. Mox is based on Elixir behaviors, which you may or may not want to be using, and it provides no mechanism for switching out the mock at runtime during your test. It leaves that up to you. And I think the idea that your source code should change just so that you can mock something is insane.

Stub HTTP with mimic

First, a quick rundown on how to set up mimic:

# add to your dependencies, run mix deps.get, etc.,
def deps do
  [
    {:mimic, "~> 1.7", only: :test}
  ]
end
# then, in your test_helper.exs, or associated setup file for your tests:

Mimic.copy(Calculator)

ExUnit.start()

Then in your test file, you can use mimic to mock stuff in an API that’s very self explanatory:

use ExUnit.Case, async: true
use Mimic

test "add" do
  Calculator
  |> expect(:add, fn x, y -> x + y end)

  assert Calculator.add(2, 3) == 5
end

What if we want to mock an HTTP call with req? Well, we can do something like this:

test "http requests" do
  Mimic.expect(Req, :get!, fn url ->
    assert url == "https://api.weather.gov/"
    %Req.Response{body: %{"description" => "mocked"}}
  end)

  response = Req.get!("https://api.weather.gov/")

  assert response.body["description"] == "mocked"
end

Note! Be careful not to do this:


  Mimic.expect(Req, :get!, fn "https://api.weather.gov/" ->
    %Req.Response{body: %{"description" => "mocked"}}
  end)

If you do this, and then try a failed expectation, like calling the url with .com instead of .gov , you’ll get:

1) test http requests (Test)
     mocks.exs:22
     ** (FunctionClauseError) no function clause matching in anonymous fn/1 in Test."test http requests"/1

     The following arguments were given to anonymous fn/1 in Test."test http requests"/1:

         # 1
         "https://api.weather.com/"

     code: response = Req.get!("https://api.weather.com/")
     stacktrace:
       mocks.exs:24: anonymous fn/1 in Test."test http requests"/1
       mocks.exs:28: (test)

The failed pattern match is, as you can see, not a super readable error. If you instead assert inside the mock as shown above, you get:

1) test http requests (Test)
     mocks.exs:22
     Assertion with == failed
     code:  assert url == "https://api.weather.gov/"
     left:  "https://api.weather.com/"
     right: "https://api.weather.gov/"
     stacktrace:
       mocks.exs:25: anonymous fn/1 in Test."test http requests"/1
       mocks.exs:29: (test)

Much more readable.

Ok, but what about blocking all HTTP requests that we don’t mock? Well, as it turns out, it’s very easy with Mimic. You can just do this:

Mimic.stub(Req)

That’s it. That’s all there is to it. Now any call to any function you don’t explicitly mock or stub in Req will fail.

You can apply this to any HTTP library of your choice:

Mimic.stub(Req)
Mimic.stub(HTTPoison)
Mimic.stub(Tesla)
Mimic.stub(Mint)
Mimic.stub(Finch)

It’s just that easy. Here’s a fuller example, given as a mix script, and showing a mix of stubbing with expectations:

Mix.install([
  :mimic,
  :req
])

Mimic.copy(Req)

ExUnit.start()

defmodule Test do
  use ExUnit.Case

  setup do
    # Block all HTTP by default.
    Mimic.stub(Req)

    :ok
  end

  test "http requests" do
    # Expect some specific request for this test.
    Mimic.expect(Req, :get!, fn url ->
      assert url == "https://api.weather.gov/"
      %Req.Response{body: %{"description" => "mocked"}}
    end)

    response = Req.get!("https://api.weather.gov/")

    assert response.body["description"] == "mocked"

    # Test that a subsequent call somewhere else hits an error.
    assert_raise Mimic.UnexpectedCallError, fn ->
      Req.get!("https://api.weather.com/")
    end
  end
end

You can put that Mimic.stub(Whatever) bit in your setup block, as here, or in some higher up test_helper.exs file to apply to your whole project. And now you’re protected from unexpected HTTP calls in your tests!