Packages

TestProbe is a tiny wrapper around GenServer, that puts testing of actor interactions under control.

Current section

Files

Jump to
test_probe lib test_probe.ex
Raw

lib/test_probe.ex

defmodule TestProbe do
@moduledoc ~S"""
`TestProbe` is a tiny wrapper around `GenServer`, that puts testing of actor
interactions under control.
The probe, once started, will accept any message (`call`, `cast`, `info`) and
put it into the queue, where it can be accessed using Probe API.
After retrieving a message you can tell the probe to respond (in case of call).
You can also send messages or execute arbitrary code on behalf of the probe.
In the docs and examples we'll use MonEx macros, you can easily choose not to use
it. `ok(x)` and `error(e)` stand for `{:ok, x}` and `{:error, e}` respectively.
Optional `some(x)` and `none()` would be `{:some, x}` and `{:none}`. Check
[monex docs](https://hexdocs.pm/monex/api-reference.html) for details.
## Examples
import MonEx.{Result, Option} # to support ok() some() etc...
ok(probe) = Probe.start()
task = Task.async(fn -> GenServer.call(probe, :hey) end)
# Here we wait for the message matching the pattern:
assert some(msg) = Probe.receive(probe, %Message{data: :hey})
IO.inspect(msg)
# %TestProbe.Message{
# data: :hey,
# from: {#PID<0.193.0>, #Reference<0.2208989138.2906914819.164457>},
# type: :call
# }
# Now you can use msg to respond:
Probe.reply(msg, :sup)
response = Task.await(task)
assert response == :sup
"""
import MonEx.Option
alias TestProbe.Message
@doc """
Starts the probe with optionals parameters.
Parameters passed directly to GenServer.
`name` can be useful, if you're testing some code accessing another process by
name.
Returns: `ok(pid)`
"""
@spec start(Keyword.t) :: MonEx.Result.t
def start(opts \\ []) do
GenServer.start(TestProbe.Server, [], opts)
end
@doc """
Stops the probe.
"""
@spec stop(pid) :: atom
def stop(probe) do
Agent.stop(probe)
end
@doc """
Polls the probe to check if there is a message matching pattern provided.
If message is not in a queue yet, will wait for a `timeout` time. When timeout has
passed, a `none()` will be returned.
Returns: `some(message)` or `none()`
"""
@spec receive(pid, TestProbe.Message.t, integer) :: MonEx.Option.t
def receive(probe, message, timeout \\ 1000) do
GenServer.call(probe, {:probe_receive, message, timeout}, 2 * timeout)
end
@doc """
Pulls all messages from the probe.
Be careful, if you call this right after sending a message from some other
process, there's no guarantee that this message will be there. To provide
that guarantee, confirm reception with `receive` first.
Returns: `[message, ...]`
"""
@spec all_received(pid) :: list(TestProbe.Message.t)
def all_received(probe) do
GenServer.call(probe, :probe_dump)
end
@doc """
Returns last message received by the probe.
Just like with `all_received`, the very last message can not be there yet at
the time of calling. Consider using `receive`.
Returns: `some(message)` or `none()`
"""
@spec last_received(pid) :: MonEx.Option.t
def last_received(probe) do
all_received(probe) |> List.first |> to_option
end
@doc """
Returns how many times the message matching the pattern was received.
Returns: `amount`
"""
@spec times_received(pid, TestProbe.Message.t) :: integer
def times_received(probe, message) do
all_received(probe)
|> Enum.reduce(0, fn msg, count ->
if Message.match(message, msg), do: count + 1, else: count
end)
end
@doc """
Sends a response to provided message.
Returns: `ok(message)` or `error(message)` if operation failed
"""
@spec reply(pid, TestProbe.Message.t, term) :: term
def reply(probe, message, response) do
GenServer.call(probe, {:probe_reply, message, response})
end
@doc """
Runs arbitrary lambda in the context of probe.
This in particular is used to implement sending messages on behalf of the probe.
run probe, fn ->
GenServer.cast(pid, message)
end
Returns: `ok(message)` or `error(message)` if operation failed
"""
@spec run(pid, (() -> term)) :: term
def run(probe, fun) do
GenServer.call(probe, {:probe_run, fun})
end
@doc """
Sends `cast` on behalf of the probe.
"""
@spec cast(pid, pid, term) :: term
def cast(probe, pid, message) do
run probe, fn ->
GenServer.cast(pid, message)
end
end
@doc """
Sends `call` on behalf of the probe and returns the result.
"""
@spec call(pid, pid, term) :: term
def call(probe, pid, message) do
run probe, fn ->
GenServer.call(pid, message)
end
end
@doc """
Sends `info` on behalf of the probe.
"""
@spec send(pid, pid, term) :: term
def send(probe, pid, message) do
run probe, fn ->
send pid, message
end
end
end