kirschbaum-development/pest-plugin-realtime
A Pest plugin that brings realtime broadcasting assertions to your Laravel tests, letting you verify events are broadcast instantly over Reverb or Pusher. Write expressive expectations for channels, payloads, and event order without boilerplate setup.
Prerequisites. You need Pest (not PHPUnit-native) as your test runner, a Laravel app with an actual broadcasting surface (events, channels, presence/auth), and a decision up front on whether tests drive a real broadcaster (Reverb/Soketi/Pusher-compatible) or an in-process fake. That decision drives almost all of the setup cost.
Minimal steps:
composer require --dev kirschbaum-development/pest-plugin-realtime:0.6.2
pest-plugin type; if nothing changes, check vendor/kirschbaum-development/pest-plugin-realtime/composer.json for the extra.pest.plugins entry and verify it's registered in tests/Pest.php if manual registration is required.composer why-not against your pinned Pest/PHP/Laravel versions, then run your existing suite untouched to confirm the plugin doesn't alter output or exit codes.Where to look first: the package's src/ and its own tests/ directory. The published docs are thin (see Gotchas), and the test suite is the most reliable source of truth for the actual assertion API and its semantics.
First use case: a private-channel broadcast, because it exercises the two things that break in realtime apps — event delivery and channel authorization:
it('delivers order updates to the private account channel', function () {
$user = User::factory()->create();
$order = Order::factory()->for($user->account)->create();
// Illustrative — confirm exact assertion names in the package source.
realtime()->actingAs($user)
->subscribe("private-account.{$user->account_id}")
->expectBroadcast(OrderUpdated::class);
$order->update(['status' => 'shipped']);
realtime()->assertBroadcastReceived(OrderUpdated::class);
});
Start with a test that currently passes under Broadcast::fake(). Porting a known-green test isolates plugin bugs from application bugs.
Port a representative triad, not the whole suite. Three tests prove the value case and bound the risk: a public broadcast, a private/presence authorization case, and a negative case (unauthorized subscriber must not receive). Only port more once flake rate is measured.
Test the seams mocks can't reach. The plugin's differentiator is dropped events and connection recovery — the failure modes Broadcast::fake() structurally cannot model. Keep the bulk of your suite on fakes and reserve this plugin for the thin layer of tests that need real transport semantics:
it('recovers and replays events missed during a disconnect', function () {
realtime()->subscribe('private-orders');
realtime()->simulateDisconnect();
OrderShipped::dispatch($order); // expected to be dropped
realtime()->simulateReconnect();
realtime()->assertEventReplayed(OrderShipped::class, times: 1);
realtime()->assertNoDuplicateEvents();
});
Use Pest datasets for channel-type matrices. Public/private/presence share one delivery contract with different auth rules — datasets keep that DRY:
it('respects channel authorization', function (string $channel, bool $allowed) {
// ...
})->with([
['orders.public', true],
['private-account.1', false], // wrong subscriber
['presence-room.42', true],
]);
Lifecycle discipline via Pest hooks. Any real-broadcaster setup belongs in beforeEach/afterEach (or a shared trait), never inline in tests — orphaned processes are the #1 source of CI poisoning:
beforeEach(fn () => $this->realtime = startRealtimeHarness());
afterEach(fn () => $this->realtime->shutdown());
CI integration. If a live server is required: containerize it, allocate per-worker ports (Pest's --parallel multiplies this problem), block on a readiness probe rather than sleep, and ensure teardown runs on failure. Wire it as a non-blocking job first.
Rollout sequencing. Phase 0: read the v0.6.1→v0.6.2 diff and check the compatibility matrix. Phase 1: one repo, 2–3 ported tests, run ~20 CI iterations and record flake rate plus added wall-clock. Phase 2: pin, document the harness, name an owner. Phase 3: expand. Do not gate merges on new realtime tests until the flake profile is known.
Escape hatch. Keep every plugin test conceptually rewritable as Broadcast::fake() + Event::fake() assertions, so an abandoned upstream costs you a day, not a quarter.
The description is genuinely ambiguous — resolve it first. The repo tagline says "test realtime browser behavior, dropped events, and connection recovery," while other package copy describes streaming live pass/fail output during runs. Those are entirely different tools. Read src/ before buying into either framing; if it's the latter, most of the CI-harness cost below does not apply.
Pre-1.0 with uninformative changelogs. v0.6.2 ships only an auto-generated compare link — no notes, no migration guidance. Patch bumps are likely safe; minor bumps may not be. Pin exact versions, and treat every release as requiring a diff read. Budget a short review per release and assign a named owner so it doesn't rot silently.
Flakiness is the dominant risk, not correctness. Realtime assertions are inherently timing-sensitive. Ban sleep() from these tests; use the plugin's deterministic hooks where they exist and bounded polling helpers where they don't. A flaky test that gets retried blindly is worse than no test.
Guard against false greens. The most dangerous failure is an assertion passing because the broadcaster never connected — not because behavior is right. Once per setup, deliberately break the realtime path (stop the server, misconfigure a channel) and confirm the test goes red. Add that as a periodic sanity check, not a one-off.
Port collisions and orphaned processes under --parallel. Each worker needs its own port and its own server lifecycle. Verify teardown with ps/ss after a failing run — zombie servers will silently pass tests against stale state, then break the next run mysteriously.
Don't let it duplicate or shadow the fakes. If most of your "realtime tests" are effectively thin wrappers over Broadcast::fake() semantics, the plugin is not earning its CI complexity. Keep the split explicit: fakes for unit-level event-shape assertions, plugin for transport/auth/recovery behavior.
Debugging a single flake: isolate with pest --filter='recovers and replays' (never the full suite — parallel workers confound the signal), drop --parallel to 1, and capture both the plugin's output and the broadcaster's server log side by side. Silent behavioral drift between the plugin's simulation and real broadcaster semantics — especially presence events and channel auth — is the subtle bug class here.
Extension points to reach for before forking: Pest's beforeEach/afterEach for harness lifecycle, custom expectations via expect()->extend() for domain-specific assertions, and a shared trait or base test case for subscribe/assert boilerplate. If you find yourself fighting the plugin's API, that's a signal to contribute upstream or fall back to fakes — not to grow a private fork of a 1-star dependency.
Version-matrix drift. Confirm the plugin's CI actually tests your Pest major, PHP floor, and Laravel version. A package tested against Pest 2 while you run Pest 3 is a source of confusing, intermittent failures that look like your application's fault.
How can I help you explore Laravel packages today?