`Req.Test` stubs keep HTTP out of your tests

almirsarajcic

almirsarajcic

2 hours ago

0 comments

Req ships with a built-in test stub mechanism. No Bypass, no Mox, no additional dependencies — just three small changes and your HTTP calls never hit the network in tests.

# 1. lib/my_app/weather_client.ex — read options from config
defmodule MyApp.WeatherClient do
  def get_forecast(city) do
    req_options = Application.get_env(:my_app, :weather_req_options, [])

    Req.new(base_url: "https://api.weather.com")
    |> Req.merge(req_options)
    |> Req.get!(url: "/forecast", params: [city: city])
  end
end

# 2. config/test.exs — point the plug at Req.Test
config :my_app, :weather_req_options,
  plug: {Req.Test, MyApp.WeatherClient},
  retry: false

# 3. test — stub and assert
test "get_forecast/1 returns the forecast for a city" do
  Req.Test.stub(MyApp.WeatherClient, fn conn ->
    Req.Test.json(conn, %{"forecast" => "sunny", "city" => "London"})
  end)

  response = MyApp.WeatherClient.get_forecast("London")

  assert response.body["forecast"] == "sunny"
end

The stub receives a Plug.Conn — the same conn you know from Phoenix. Req.Test.json/2 is a convenience that sets the content-type header and JSON-encodes the response body for you.

The key insight is that plug: {Req.Test, MyApp.WeatherClient} acts as a transport-level interceptor. Instead of opening a TCP connection, Req calls your plug function directly. The test process owns the stub, so concurrent tests with async: true are safe without any extra setup.

You can inspect the outgoing request inside the stub too — useful when you want to assert on what your code is actually sending:

Req.Test.stub(MyApp.WeatherClient, fn conn ->
  assert conn.params["city"] == "London"
  Req.Test.json(conn, %{"forecast" => "sunny"})
end)

And Req.Test.transport_error/2 lets you simulate network failures to test your error handling:

Req.Test.stub(MyApp.WeatherClient, fn conn ->
  Req.Test.transport_error(conn, :timeout)
end)

If your Req call happens inside a spawned Task, the stub won’t be visible to the child process by default. Pass ownership explicitly with Req.Test.allow/3 — or see Task.Supervisor sandbox allowance patterns for the full setup.

Req.Test docs

Comments (0)

Sign in with GitHub to join the discussion