›
›
›
  1. docs
  2. ›
  3. byrcsc/laravel-comments
1.x
Browse documentationOpenClose

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Commentable models
  • Threads and replies
  • Moderation
  • Initial status
  • Pinning
  • Reactions
  • Edits and revisions
  • Attachments
  • Deleting comments

Operations

  • Comment counts
  • Events and listeners
  • Reply notifications
  • Authorization
  • Rendering and safety

Reference

  • Configuration
  • Console commands
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Commentable models
  • Threads and replies
  • Moderation
  • Initial status
  • Pinning
  • Reactions
  • Edits and revisions
  • Attachments
  • Deleting comments

Operations

  • Comment counts
  • Events and listeners
  • Reply notifications
  • Authorization
  • Rendering and safety

Reference

  • Configuration
  • Console commands
  • Testing
  • Troubleshooting

byrcsc/laravel-comments · 1.x

Testing.

Test comment actions with Comments::fake() or persisted model factories.

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.

What to read next

  • Events and listeners to choose between fake calls and real event assertions.
  • Authorization to test application policies around comment actions.
  • Troubleshooting for failures caused by boot-time config.
PreviousConsole commandsNextTroubleshooting
View source

On this page

  1. Comments::fake()
  2. Assertions
  3. Read what was recorded
  4. What the fake holds itself to
  5. What it refuses
  6. Factories
  7. Comment states
  8. The other three
  9. Testing against a real database
  10. What to read next