Browse documentationOpen

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 arrived

What 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();     // throws

A 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

StateEffect
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();
  • CommentReactionFactory has no default reactor and cannot have one โ€” supply it with by(). The reaction defaults to the allowlist's first entry, so a factory-built row is one the engine would also have accepted.
  • CommentRevisionFactory names 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.
  • CommentAttachmentFactory writes metadata about files that do not exist, which is exactly what an attachment row is to this package. Point it at a Storage::fake() disk with on() 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.