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 aKeyError
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 pipelines —
plug
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.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.