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.
Req already has built-in support for stubs via :plug
, :adapter
, and (indirectly) :base_url
options. This module enhances these capabilities by providing:
Stub any value with
Req.Test.stub(name, value)
and access it withReq.Test.stub(name)
. These functions can be used in concurrent tests.Access plug stubs with
plug: {Req.Test, name}
.Easily create JSON responses for Plug stubs with
Req.Test.json(conn, body)
.
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.
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