View Source Req.Test (req v0.5.0)

Req testing conveniences.

Req is composed of:

  • Req - the high-level API

  • Req.Request - the low-level API and the request struct

  • Req.Steps - the collection of built-in steps

  • Req.Test - the testing conveniences (you're here!)

Req already has built-in support for different variants of stubs via :plug, :adapter, and (indirectly) :base_url options. With this module you can:

Mocks and stubs are using the same ownership model of nimble_ownership, also used by Mox. This allows Req.Test to be used in concurrent tests.

Example

Imagine we're building an app that displays weather for a given location using an HTTP weather service:

defmodule MyApp.Weather do
  def get_rating(location) do
    case get_temperature(location) do
      {:ok, %{status: 200, body: %{"celsius" => celsius}}} ->
        cond do
          celsius < 18.0 -> {:ok, :too_cold}
          celsius < 30.0 -> {:ok, :nice}
          true -> {:ok, :too_hot}
        end

      _ ->
        :error
    end
  end

  def get_temperature(location) do
    [
      base_url: "https://weather-service"
    ]
    |> Keyword.merge(Application.get_env(:myapp, :weather_req_options, []))
    |> Req.request()
  end
end

We configure it for production:

# config/runtime.exs
config :myapp, weather_req_options: [
  auth: {:bearer, System.fetch_env!("MYAPP_WEATHER_API_KEY")}
]

In tests, instead of hitting the network, we make the request against a plug stub named MyApp.Weather:

# config/test.exs
config :myapp, weather_req_options: [
  plug: {Req.Test, MyApp.Weather}
]

Now we can control our stubs in concurrent tests:

use ExUnit.Case, async: true

test "nice weather" do
  Req.Test.stub(MyApp.Weather, fn conn ->
    Req.Test.json(conn, %{"celsius" => 25.0})
  end)

  assert MyApp.Weather.get_rating("Krakow, Poland") == {:ok, :nice}
end

Concurrency and Allowances

The example above works in concurrent tests because MyApp.Weather.get_rating/1 calls directly to Req.request/1 in the same process. It also works in many cases where the request happens in a spawned process, such as a Task, GenServer, and more.

However, if you are encountering issues with stubs not being available in spawned processes, it's likely that you'll need explicit allowances. For example, if MyApp.Weather.get_rating/1 was calling Req.request/1 in a process spawned with spawn/1, the stub would not be available in the spawned process:

# With code like this, the stub would not be available in the spawned task:
def get_rating_async(location) do
  spawn(fn -> get_rating(location) end)
end

To make stubs defined in the test process available in other processes, you can use allow/3. For example, imagine that the call to MyApp.Weather.get_rating/1 was happening in a spawned GenServer:

test "nice weather" do
  {:ok, pid} = start_gen_server(...)

  Req.Test.stub(MyApp.Weather, fn conn ->
    Req.Test.json(conn, %{"celsius" => 25.0})
  end)

  Req.Test.allow(MyApp.Weather, self(), pid)

  assert get_weather(pid, "Krakow, Poland") == {:ok, :nice}
end

Broadway

If you're using Req.Test with Broadway, you may need to use allow/3 to make stubs available in the Broadway processors. A great way to do that is to hook into the Telemetry events that Broadway publishes to manually allow the processors and batch processors to access the stubs. This approach is similar to what is documented in Broadway itself.

First, you should add the test PID (which is allowed to use the Req stub) to the metadata for the test events you're publishing:

Broadway.test_message(MyApp.Pipeline, message, metadata: %{req_stub_owner: self()})

Then, you'll need to define a test helper to hook into the Telemetry events. For example, in your test/test_helper.exs file:

defmodule BroadwayReqStubs do
  def attach(stub) do
    events = [
      [:broadway, :processor, :start],
      [:broadway, :batch_processor, :start],
    ]

    :telemetry.attach_many({__MODULE__, stub}, events, &__MODULE__.handle_event/4, %{stub: stub})
  end

  def handle_event(_event_name, _event_measurement, %{messages: messages}, %{stub: stub}) do
    with [%Broadway.Message{metadata: %{req_stub_owner: pid}} | _] <- messages do
      :ok = Req.Test.allow(stub, pid, self())
    end

    :ok
  end
end

Last but not least, attach the helper in your test/test_helper.exs:

BroadwayReqStubs.attach(MyStub)

Summary

Functions

Sends HTML response.

Sends JSON response.

Sends text response.

Simulates a network transport error.

Functions (Mocks & Stubs)

Allows pid_to_allow to access name provided that owner is already allowed.

Creates a request expectation with the given name and plug, expected to be fetched at most n times, in order.

Sets the Req.Test mode based on the given ExUnit context.

Sets the Req.Test mode to "private", meaning that stubs can be shared across tests concurrently.

Sets the Req.Test mode to "global", meaning that the stubs are shared across all tests and cannot be used concurrently.

Creates a request stub with the given name and plug.

Verifies that all the plugs expected to be executed within any scope have been executed.

Verifies that all the plugs expected to be executed within the scope of name have been executed.

Sets a ExUnit callback to verify the expectations on exit.

Functions

@spec html(Plug.Conn.t(), iodata()) :: Plug.Conn.t()

Sends HTML response.

Examples

iex> plug = fn conn ->
...>   Req.Test.html(conn, "<h1>Hello, World!</h1>")
...> end
iex>
iex> resp = Req.get!(plug: plug)
iex> resp.headers["content-type"]
["text/html; charset=utf-8"]
iex> resp.body
"<h1>Hello, World!</h1>"
@spec json(Plug.Conn.t(), term()) :: Plug.Conn.t()

Sends JSON response.

Examples

iex> plug = fn conn ->
...>   Req.Test.json(conn, %{celsius: 25.0})
...> end
iex>
iex> resp = Req.get!(plug: plug)
iex> resp.headers["content-type"]
["application/json; charset=utf-8"]
iex> resp.body
%{"celsius" => 25.0}
@spec text(Plug.Conn.t(), iodata()) :: Plug.Conn.t()

Sends text response.

Examples

iex> plug = fn conn ->
...>   Req.Test.text(conn, "Hello, World!")
...> end
iex>
iex> resp = Req.get!(plug: plug)
iex> resp.headers["content-type"]
["text/plain; charset=utf-8"]
iex> resp.body
"Hello, World!"
Link to this function

transport_error(conn, reason)

View Source (since 0.5.0)
@spec transport_error(Plug.Conn.t(), reason :: atom()) :: Plug.Conn.t()

Simulates a network transport error.

Examples

iex> plug = fn conn ->
...>   Req.Test.transport_error(conn, :timeout)
...> end
iex>
iex> Req.get(plug: plug, retry: false)
{:error, %Req.TransportError{reason: :timeout}}

Functions (Mocks & Stubs)

Link to this function

allow(name, owner, pid_to_allow)

View Source
@spec allow(name(), pid(), pid() | (-> pid())) :: :ok | {:error, Exception.t()}

Allows pid_to_allow to access name provided that owner is already allowed.

Link to this function

expect(name, n \\ 1, plug)

View Source (since 0.4.15)
@spec expect(name(), pos_integer(), plug()) :: name()

Creates a request expectation with the given name and plug, expected to be fetched at most n times, in order.

This function allows you to expect a n number of request and handle them in order via the given plug. It is safe to use in concurrent tests. If you fetch the value under name more than n times, this function raises a RuntimeError.

The name can be any term.

The plug can be one of:

  • A function plug: a fun(conn) or fun(conn, options) function that takes a Plug.Conn and returns a Plug.Conn.

  • A module plug: a module name or a {module, options} tuple.

See stub/2 and module documentation for more information.

Examples

Let's simulate a server that is having issues: on the first request it is not responding and on the following two requests it returns an HTTP 500. Only on the third request it returns an HTTP 200. Req by default automatically retries transient errors (using Req.Steps.retry/1) so it will make multiple requests exercising all of our request expectations:

iex> Req.Test.expect(MyStub, &Req.Test.transport_error(&1, :econnrefused))
iex> Req.Test.expect(MyStub, 2, &Plug.Conn.send_resp(&1, 500, "internal server error"))
iex> Req.Test.expect(MyStub, &Plug.Conn.send_resp(&1, 200, "ok"))
iex> Req.get!(plug: {Req.Test, MyStub}).body
# 15:57:06.309 [error] retry: got exception, will retry in 1000ms, 3 attempts left
# 15:57:06.309 [error] ** (Req.TransportError) connection refused
# 15:57:07.310 [error] retry: got response with status 500, will retry in 2000ms, 2 attempts left
# 15:57:09.311 [error] retry: got response with status 500, will retry in 4000ms, 1 attempt left
"ok"

iex> Req.request!(plug: {Req.Test, MyStub})
** (RuntimeError) no mock or stub for MyStub
Link to this function

set_req_test_from_context(context \\ %{})

View Source (since 0.5.0)
@spec set_req_test_from_context(ex_unit_context :: term()) :: :ok

Sets the Req.Test mode based on the given ExUnit context.

This works as a ExUnit callback:

setup :set_req_test_from_context
Link to this function

set_req_test_to_private(context \\ %{})

View Source (since 0.5.0)
@spec set_req_test_to_private(ex_unit_context :: term()) :: :ok

Sets the Req.Test mode to "private", meaning that stubs can be shared across tests concurrently.

Link to this function

set_req_test_to_shared(context \\ %{})

View Source (since 0.5.0)
@spec set_req_test_to_shared(ex_unit_context :: term()) :: :ok

Sets the Req.Test mode to "global", meaning that the stubs are shared across all tests and cannot be used concurrently.

@spec stub(name(), plug()) :: :ok

Creates a request stub with the given name and plug.

Req allows running requests against plugs (instead of over the network) using the :plug option. However, passing the :plug value throughout the system can be cumbersome. Instead, you can tell Req to find plugs by name by setting plug: {Req.Test, name}, and register plug stubs for that name by calling Req.Test.stub(name, plug). In other words, multiple concurrent tests can register test stubs under the same name, and when Req makes the request, it will find the appropriate implementation, even when invoked from different processes than the test process.

The name can be any term.

The plug can be one of:

  • A function plug: a fun(conn) or fun(conn, options) function that takes a Plug.Conn and returns a Plug.Conn.

  • A module plug: a module name or a {module, options} tuple.

Examples

iex> Req.Test.stub(MyStub, fn conn ->
...>   send(self(), :req_happened)
...>   Req.Test.json(conn, %{})
...> end)
:ok
iex> Req.get!(plug: {Req.Test, MyStub}).body
%{}
iex> receive do
...>   :req_happened -> :ok
...> end
:ok
@spec verify!() :: :ok

Verifies that all the plugs expected to be executed within any scope have been executed.

Link to this function

verify!(name)

View Source (since 0.5.0)
@spec verify!(name()) :: :ok

Verifies that all the plugs expected to be executed within the scope of name have been executed.

Link to this function

verify_on_exit!(context \\ %{})

View Source (since 0.5.0)
@spec verify_on_exit!(term()) :: :ok

Sets a ExUnit callback to verify the expectations on exit.

Similar to calling verify!/0 at the end of your test.