Test delivery mode: assert what your application pushed without touching APNS or FCM.
Set delivery: :test in your test config and every send — PushX.push/4,
push_data/4, batches, PushX.APNS/PushX.FCM directly, and named
instances — is validated exactly as in production (required :topic,
target format, :mode, payload encoding and size limits) and then
recorded and answered locally with {:ok, %PushX.Response{status: :sent}}
instead of being sent. No credentials are needed; retries, the circuit
breaker and rate limiter are not involved.
# config/test.exs
config :pushx, delivery: :testRecorded pushes are scoped to the test process: a test only sees the
pushes made by itself and by processes it started (Task, Task.Supervisor
children including push_batch/4 workers, and anything else that carries
$callers), so async: true tests do not interfere. Records are deleted
automatically when the test process exits, so memory stays bounded and no
clear/0 between tests is needed.
Asserting
use ExUnit.Case, async: true
import PushX.Test.Assertions
test "order shipped notifies the customer" do
MyApp.Orders.ship(order)
push = assert_pushed(%{provider: :apns, target: ^device_token})
assert push.payload["aps"]["alert"]["title"] == "Order shipped"
assert push.opts[:topic] == "com.example.app"
refute_pushed(%{provider: :fcm})
endassert_pushed/1 and refute_pushed/1 take a pattern (pins allowed) that
is matched against each recorded PushX.Test.Push.t/0; pushes/0 returns
the raw list when you'd rather filter yourself.
Scripting failures
Use stub/1 to make specific pushes fail — the idiomatic way to test token
cleanup, since :on_invalid_token and PushX.Response.should_remove_token?/1
behave exactly as with a real provider response:
test "dead tokens are deleted" do
PushX.Test.stub(fn
%{target: "dead-token"} -> {:error, :unregistered}
_push -> :ok
end)
MyApp.Notifier.broadcast("Hi")
assert_receive {:token_deleted, "dead-token"}
endThe stub receives the PushX.Test.Push.t/0 and returns :ok (delivered),
{:error, status} (a PushX.Response error with that status), or a full
{:ok, %PushX.Response{}} / {:error, %PushX.Response{}}. Stubs are per
test process too.
Named instances in tests
PushX.Instance.start/3 validates credentials before starting, so use the
throwaway-key helpers rather than committing keys. In test delivery mode
no Goth process is started for FCM instances (nothing contacts Google):
PushX.Instance.start(:tenant_apns, :apns,
key_id: "TEST", team_id: "TEST", private_key: PushX.Test.apns_private_key(), mode: :sandbox)
PushX.Instance.start(:tenant_fcm, :fcm,
project_id: "tenant", credentials: PushX.Test.fcm_credentials())In test delivery mode the instances never contact the providers.
What is not simulated
Test mode answers after local validation, so it cannot tell you what Apple
or Google would have said about a token or payload — only that PushX would
have sent it. Use stub/1 to model provider responses you care about.
Summary
Functions
True when delivery: :test is configured.
A freshly generated P-256 EC private key (PEM), the kind APNS signs with. Generated once per VM and cached; tied to no Apple account. For starting APNS instances in tests.
Forgets the current test process's recorded pushes and stub.
A service-account credentials map with a freshly generated RSA key, valid
for PushX.Instance.start/3's credential validation. Generated once per VM
and cached; tied to no Google project.
The most recent recorded push for the current test process, or nil.
All pushes recorded for the current test process (and processes it spawned), in recording order — for concurrent batch workers that is completion order, not input order.
Scripts responses for the current test process. fun receives each
PushX.Test.Push.t/0 (with result: nil) and returns
Functions
@spec active?() :: boolean()
True when delivery: :test is configured.
@spec apns_private_key() :: String.t()
A freshly generated P-256 EC private key (PEM), the kind APNS signs with. Generated once per VM and cached; tied to no Apple account. For starting APNS instances in tests.
@spec clear() :: :ok
Forgets the current test process's recorded pushes and stub.
Not required between tests (records are per process), but handy inside a test that exercises several scenarios.
@spec fcm_credentials() :: map()
A service-account credentials map with a freshly generated RSA key, valid
for PushX.Instance.start/3's credential validation. Generated once per VM
and cached; tied to no Google project.
@spec last_push() :: PushX.Test.Push.t() | nil
The most recent recorded push for the current test process, or nil.
@spec pushes() :: [PushX.Test.Push.t()]
All pushes recorded for the current test process (and processes it spawned), in recording order — for concurrent batch workers that is completion order, not input order.
@spec stub( (PushX.Test.Push.t() -> :ok | {:error, atom()} | {:ok, PushX.Response.t()} | {:error, PushX.Response.t()}) | nil ) :: :ok
Scripts responses for the current test process. fun receives each
PushX.Test.Push.t/0 (with result: nil) and returns:
:ok— delivered (the default when no stub is set){:error, status}— aPushX.Responseerror with that status atom{:ok, %PushX.Response{}}or{:error, %PushX.Response{}}— verbatim
Pass nil to remove the stub.