When testing Elixir applications that make HTTP requests, you often want to assert what the request looks like — not just its response. The Req HTTP client provides powerful testing features through Req.Test, allowing you to intercept and inspect HTTP requests without making actual network calls.

In this post, we'll explore how to:

  • Inspect and assert request headers
  • Decode and assert the JSON body
  • Use Req.Test to keep your tests fast, isolated, and deterministic

The Setup: your HTTP client code

Let's say you're testing this function:

defmodule MyApp.Jinja do
  def parse_url(url, opts \\ []) do
    opts =
      [
        url: "https://r.jina.ai/",
        headers: [{"accept", "application/json"}],
        json: %{"url" => url}
      ]
      |> Keyword.merge(opts)

    Req.post!(opts).body["data"]
  end
end

This function POSTs a JSON payload to a remote API and returns the "data" key from the response body.

The key here is that you can pass additional options to the function which get merged into the Req options.

Testing with Req.Test

To test this, use Req.Test with a custom plug function that intercepts and inspects the request.

defmodule MyApp.JinjaTest do
  use ExUnit.Case, async: true

  alias MyApp.Jinja

  test "parse_url/1 sends correct headers and body" do
    plug = fn conn ->
      # Read request body from Req.Test adapter
      body = conn.adapter |> elem(1) |> Map.fetch!(:req_body)
      decoded = Jason.decode!(body)

      # Assert JSON body
      assert decoded == %{"url" => "https://example.com"}

      # Assert headers
      assert {"content-type", "application/json"} in conn.req_headers
      assert {"accept", "application/json"} in conn.req_headers

      # Return mock response
      Req.Test.json(conn, %{"data" => %{"foo" => "bar"}})
    end

    result = Jinja.parse_url("https://example.com", plug: plug)
    assert result == %{"foo" => "bar"}
  end
end

Behind the scenes

When using Req.Test, the conn passed into the plug is a Plug.Conn struct with some key differences:

  • The request body is already loaded and stored in the adapter data:

    conn.adapter
    # => {Plug.Adapters.Test.Conn, %{req_body: "..."}}
    
  • Trying to use Plug.Conn.read_body/2 will raise a KeyError because there's no :body key in the struct — the body was never streamed.

To safely read it, use:

conn.adapter |> elem(1) |> Map.fetch!(:req_body)

Why this approach rocks

  • No external HTTP calls — your tests stay fast and reliable.
  • Full introspection — you can inspect headers, method, URL, body, etc.
  • Works with Req pipelinesplug intercepts at the request level.

Bonus: cleaner helper

If you use this pattern a lot, extract a helper:

defmodule MyApp.TestHelpers do
  def read_req_body(conn) do
    conn.adapter |> elem(1) |> Map.fetch!(:req_body)
  end
end

Then use:

body = read_req_body(conn)

Conclusion

Req.Test gives you an elegant and powerful way to test outgoing HTTP requests in Elixir. By inspecting the conn, you can assert exactly what your app sends over the wire — without ever leaving your test suite.