Faking with Messages::fake()
Messages::fake() swaps in a recording fake and returns it as the handle for your assertions:
use RoundlyConsulting\Messages\Facades\Messages;
$fake = Messages::fake();
$alice->sendMessageTo($thread, 'Hi'); // model traits are recorded too
Messages::thread($thread)->rename('Launch', by: $alice);
$fake->assertSent('Hi', to: $thread);
$fake->assertThreadRenamed($thread, to: 'Launch');
$fake->assertNothingDeleted();The fake is a MessagesManager subtype, so constructor-injected managers receive it too. It still performs every operation — rows are written, events fire — and records each one that succeeds, whichever door it came through: the facade, an injected manager, the builders, the handles, the HasMessaging trait, Thread::markReadFor() and typing(), Participant::markAsRead() or the InteractsWithMessaging helpers. Work an action does on its own behalf — the participants start() adds, system messages — is not recorded separately, and actions you call directly bypass the fake.
Every assertion
Each assertion has an assertNothing… counterpart. Optional arguments narrow the match; models are compared with Model::is():
use RoundlyConsulting\Messages\Enums\ParticipantRole;
$fake->assertThreadCreated('Launch'); // start()->create(), or a direct() that created one
$fake->assertNothingCreated();
$fake->assertSent('Hi', to: $thread); // both arguments optional
$fake->assertSentCount(2);
$fake->assertNothingSent();
$fake->assertThreadRenamed($thread, to: 'Launch crew');
$fake->assertNothingRenamed();
$fake->assertThreadArchived($thread);
$fake->assertNothingArchived();
$fake->assertMarkedRead($thread, by: $bob);
$fake->assertNothingMarkedRead();
$fake->assertTyping($thread, $bob);
$fake->assertNothingTyping();
$fake->assertParticipantAdded($thread, $carol); // new rows only, not a re-add
$fake->assertNothingAdded();
$fake->assertParticipantRemoved($thread, $carol); // remove() or leave()
$fake->assertNothingRemoved();
$fake->assertRoleChanged($thread, $carol, ParticipantRole::Admin);
$fake->assertNothingRoleChanged();
$fake->assertOwnershipTransferred($thread, to: $bob);
$fake->assertNothingTransferred();
$fake->assertEdited($message, 'fixed typo');
$fake->assertNothingEdited();
$fake->assertDeleted($message);
$fake->assertNothingDeleted();
$fake->assertPruned(days: 30);
$fake->assertNothingPruned();assertThreadCreated() also counts a direct() that had to create its thread, and assertParticipantAdded() counts new rows only, never a re-add. For custom checks, recorded() returns every recorded MessagingCall, optionally filtered by operation:
use RoundlyConsulting\Messages\Enums\MessagingOperation;
$sends = $fake->recorded(MessagingOperation::Send); // list<MessagingCall>, in call order
$sends[0]->thread; // the Thread
$sends[0]->text; // the body
$sends[0]->result; // the Message the call returnedFactory states
use RoundlyConsulting\Messages\Enums\ParticipantRole;
use RoundlyConsulting\Messages\Models\Message;
use RoundlyConsulting\Messages\Models\Participant;
use RoundlyConsulting\Messages\Models\Thread;
$thread = Thread::factory()->public()->create(); // also ->private(), ->direct(), ->open()
Message::factory()->inThread($thread)->system()->create();
Participant::factory()->inThread($thread)->owner()->read()->create();
Participant::factory()->inThread($thread)->role(ParticipantRole::Admin)->unread()->create();Participant factories default to a participant_type of user with a random id — pass participant_type and participant_id for a real model. read() leaves the row as Messages::markRead() would: the pointer on the thread’s newest message and read_at stamped; unread() clears both.
Helpers and Pest expectations
For host-app test suites, the InteractsWithMessaging trait adds fluent helpers — all through the manager, so the fake records them — and MessageExpectations registers Pest expectations:
// tests/Pest.php
RoundlyConsulting\Messages\Testing\MessageExpectations::register();
// in a test
uses(RoundlyConsulting\Messages\Testing\InteractsWithMessaging::class);
$thread = $this->startConversation($alice, $bob);
$this->actingAsParticipant($alice)->sendMessageAs($thread, 'Hi');
expect($thread)
->toHaveSentMessage('Hi')
->toHaveParticipant($bob)
->toHaveUnread($bob)
->toHaveRole($alice, ParticipantRole::Owner);
expect($reply)->toBeReplyTo($original);- actingAsParticipant($model) — remember the actor for sendMessageAs() and markReadAs().
- startConversation(...$models) — a group thread, the first model as owner.
- directThread($a, $b) — find or create the DM.
- sendMessageAs($thread, $body, ?$actor) and markReadAs($thread, ?$actor) — throw if no actor is set or passed.
- toHaveSentMessage(?$body), toHaveParticipant(), toHaveUnread(), toHaveRole() on a Thread; toBeReplyTo() on a Message.
A complete test
use RoundlyConsulting\Messages\Testing\InteractsWithMessaging;
uses(InteractsWithMessaging::class);
it('tracks unread state in a direct thread', function () {
$alice = User::factory()->create();
$bob = User::factory()->create();
$thread = $this->directThread($alice, $bob);
$this->actingAsParticipant($alice)->sendMessageAs($thread, 'Hi Bob');
expect($thread)
->toHaveSentMessage('Hi Bob')
->toHaveUnread($bob);
$this->markReadAs($thread, $bob);
expect($bob->unreadCount())->toBe(0);
});Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.