byrcsc/laravel-comments ยท 1.x
Testing.
Comments::fake() for asserting what your code asked for, and the four factories for building state a test needs to read.
Two tools, for two different tests.
Comments::fake() is for testing your code โ a controller that comments on
something, a job that reacts to something โ without touching the database. The
factories are for building the state a test needs to read.
Comments::fake()
use ByRcsc\LaravelComments\Comments;
Comments::fake();
$this->post("/posts/{$post->id}/comments", ['body' => 'Nice']);
Comments::fake()->assertCommentedOn($post);Comments::fake() starts recording and hands back the recorder. Calling it
again returns the same one, so a test can arrange and assert without holding a
variable.
Comments, replies, and reactions are held in memory instead of being written.
Tests rebuild the container between cases, so there is nothing to clean up;
Comments::stopFaking() exists for the rare test that needs the real engine
back partway through.
Assertions
$fake = Comments::fake();
$fake->assertCommented();
$fake->assertCommented(fn (Comment $c) => $c->body === 'Nice');
$fake->assertCommentedOn($post);
$fake->assertCommentedOn($post, fn (Comment $c) => $c->parent_id === null);
$fake->assertReplied();
$fake->assertReplied(fn (Comment $c) => $c->body === 'Agreed');
$fake->assertReacted();
$fake->assertReacted($user, '๐');
$fake->assertNothingCommented();
$fake->assertNothingReacted();Read what was recorded
Reads are not faked. A relation or a scope still queries a database the fake left empty, so ask the recorder instead:
$fake->comments(); // every comment written, replies included
$fake->commentsOn($post); // this commentable's
$fake->replies(); // only the ones with a parent
$fake->repliesTo($comment); // this comment's direct replies
$fake->reactionsOn($comment); // ['๐', '๐'] in the order they arrivedWhat the fake holds itself to
A faked write passes the same three creation rules a real one does: the body
length limit, the depth limit, and initial status resolution. An assertion about
a faked guest comment landing pending means something because of that.
Reacting twice with the same reaction stays a no-op and hands back the same row, exactly as the engine does.
What it refuses
Three writes are faked: comments, replies, and reactions. Everything else is
refused outright with a NotFakeableException while the fake is recording:
$comment->approve(); // throws
$comment->edit('...'); // throws
$comment->pin(); // throws
$comment->attach(...); // throws
$comment->delete(); // throwsA recorded comment carries a key no table has. Letting one of those through would write against nothing and say it worked, or fail on a foreign key three frames away from the call that caused it. A test about moderation, editing, pinning, attaching, or deleting wants a real database.
No package events fire under the fake either, because nothing happened.
Factories
Four factories, one per model. The comment factory has no commentable of its own โ the package ships no model to point one at โ so supply yours:
use ByRcsc\LaravelComments\Models\Comment;
Comment::factory()->forCommentable($post)->create();The default identity is a guest, which is the one authorship the package can invent without a host model.
Comment states
| State | Effect |
|---|---|
forCommentable($post) | Puts the comment on a record |
by($user) | An authenticated comment, clearing the guest identity |
guest($name, $email) | A guest comment; both arguments optional |
replyTo($comment) | A reply in that comment's thread, on the same commentable |
threaded($depth) | A comment $depth levels down a freshly built thread |
status($status) | A named status |
pending() | Shortcut for status(CommentStatus::Pending) |
approved() | Shortcut |
rejected() | Shortcut |
spam() | Shortcut |
pinned($at) | Pinned, optionally at a given moment |
trashed($at) | A tombstone, keeping its replies and history |
Comment::factory()->forCommentable($post)->by($user)->approved()->create();
Comment::factory()->forCommentable($post)->threaded(2)->create();threaded() builds the ancestors above the comment too, so chain it after
forCommentable() โ that is what tells them where to live. Calling it without
one throws a LogicException saying so.
No status is set by default, so factory-built comments resolve theirs exactly as
written ones do: a guest lands pending, which is what a test of a moderation
queue should be seeing. The depth limit applies here as everywhere.
The other three
use ByRcsc\LaravelComments\Models\CommentAttachment;
use ByRcsc\LaravelComments\Models\CommentReaction;
use ByRcsc\LaravelComments\Models\CommentRevision;
CommentReaction::factory()->forComment($comment)->by($user)->reaction('๐')->create();
CommentRevision::factory()->forComment($comment)->by($editor)->create();
CommentAttachment::factory()->forComment($comment)->on('uploads')->image()->create();CommentReactionFactoryhas no default reactor and cannot have one โ supply it withby(). The reaction defaults to the allowlist's first entry, so a factory-built row is one the engine would also have accepted.CommentRevisionFactorynames no editor by default, which is the ordinary case. Seeding history directly is for tests about reading it; a test about recording it should edit the comment and let the engine file the row.CommentAttachmentFactorywrites metadata about files that do not exist, which is exactly what an attachment row is to this package. Point it at aStorage::fake()disk withon()and write the file yourself when the test is about what your application stored.image()swaps the default PDF for a WebP.
Testing against a real database
For anything the fake refuses โ moderation, edits, pins, attachments, deletion, counts, events โ write against a real database and assert normally:
use ByRcsc\LaravelComments\Events\CommentApproved;
use Illuminate\Support\Facades\Event;
Event::fake([CommentApproved::class]);
$comment = Comment::factory()->forCommentable($post)->pending()->create();
$comment->approve(by: $moderator);
Event::assertDispatched(CommentApproved::class);
expect($comment->fresh()->status)->toBe(CommentStatus::Approved);Two things to watch when testing counts: read the column from the database rather than the model in hand, and remember that a status written through the query builder will not move it. See comment counts.
Config changes that are read at boot โ table names, the actor key type โ need
the application rebuilt to take effect. Everything else, including the
notification switch and the reaction allowlist, is read per call and responds to
a plain config()->set() mid-test.