View Source Req.Test (req v0.4.14)

Functions for creating test stubs.

Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.

"Mocks Aren't Stubs" by Martin Fowler

Req already has built-in support for stubs via :plug, :adapter, and (indirectly) :base_url options. This module enhances these capabilities by providing:

This module bases stubs on the ownership model of nimble_ownership, also used by Mox for example. 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

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

Sends JSON response.

Returns the stub created by stub/2.

Creates a stub with given name and value.

Functions

Link to this function

allow(stub_name, owner, pid_to_allow)

View Source

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

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}

Returns the stub created by stub/2.

Creates a stub with given name and value.

This function allows stubbing any value and later access it with stub/1. It is safe to use in concurrent tests.

See module documentation for more examples.

Examples

iex> Req.Test.stub(MyStub, :foo)
iex> Req.Test.stub(MyStub)
:foo
iex> Task.async(fn -> Req.Test.stub(MyStub) end) |> Task.await()
:foo