How to test code that calls ExNtfy without hitting a real ntfy server.
Stub the HTTP layer with Req.Test
Every ExNtfy function accepts client options, and req_options: is merged
into the underlying Req request last — so you can
inject a Req.Test plug exactly like
ExNtfy's own suite does:
defmodule MyApp.AlertsTest do
use ExUnit.Case, async: true
test "publishes a disk alert" do
Req.Test.stub(MyApp.NtfyStub, fn conn ->
assert conn.method == "POST"
{:ok, body, conn} = Plug.Conn.read_body(conn)
decoded = JSON.decode!(body)
assert decoded["topic"] == "alerts"
assert decoded["priority"] == 5
Req.Test.json(conn, %{
"id" => "xE73Iyuabi",
"time" => 1_673_542_291,
"event" => "message",
"topic" => "alerts",
"message" => decoded["message"]
})
end)
opts = [req_options: [plug: {Req.Test, MyApp.NtfyStub}]]
assert {:ok, message} = MyApp.Alerts.disk_full("db-01", opts)
assert message.id == "xE73Iyuabi"
end
endA practical pattern: have your wrapper module accept options and default them from config, so tests inject the plug and production injects nothing:
defmodule MyApp.Alerts do
def disk_full(host, opts \\ []) do
ExNtfy.publish("alerts", "Disk almost full on #{host}",
[priority: :urgent] ++ opts ++ Application.get_env(:my_app, :ntfy_opts, [])
)
end
endSimulating failures
# ntfy error responses
Req.Test.stub(MyApp.NtfyStub, fn conn ->
conn
|> Plug.Conn.put_status(429)
|> Req.Test.json(%{"code" => 42_901, "http" => 429, "error" => "limit reached"})
end)
# transport errors (pass retry: false to skip Req's transient retries)
Req.Test.stub(MyApp.NtfyStub, fn conn ->
Req.Test.transport_error(conn, :timeout)
end)Both surface as {:error, %ExNtfy.Error{}} — the first with code/http
set, the second with the exception in reason.
Polling works the same way
Poll responses are ndjson — one JSON object per line:
Req.Test.stub(MyApp.NtfyStub, fn conn ->
body =
[%{"id" => "a1", "time" => 1, "event" => "message", "topic" => "t", "message" => "hi"}]
|> Enum.map_join("\n", &JSON.encode!/1)
conn
|> Plug.Conn.put_resp_content_type("application/x-ndjson")
|> Plug.Conn.resp(200, body)
end)Streaming subscriptions
Req.Test plugs can't exercise long-lived chunked connections — for
subscription behavior you need a real server in the test, e.g.
Bypass with Plug.Conn.send_chunked/2. In
practice you rarely need that: put your logic in an ExNtfy.Handler (or the
process receiving {:ntfy, pid, message}) and unit-test it by calling the
callbacks directly with %ExNtfy.Message{} structs — the reconnect/delivery
machinery is ExNtfy's responsibility and is covered by its own suite.
If you do want end-to-end subscription tests, ExNtfy's repository is the
reference: test/ex_ntfy/subscription_test.exs shows the Bypass streaming
setup, including the exit-trapping its handlers need.