---
title: "Testing — Reviews for Laravel | Roundly"
description: "Reviews::fake() records every mutation from any path while still performing it — ten assertions, Pest matchers, a test-case trait and factories."
url: https://roundly-consulting.com/open-source/docs/reviews-for-laravel/testing
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Reviews for Laravel](https://roundly-consulting.com/open-source/docs/reviews-for-laravel.md)

# Testing

Reviews::fake() swaps in a recording ReviewsFake — a subtype of ReviewsManager, so an injected manager gets it too — and returns it. Operations still run against the database, so reads, aggregates and events behave normally, while every mutation is recorded — whether it came through the facade, an injected manager, a for()->by() builder, the HasReviews / CanVoteOnReviews traits or a Review model method:

```php
use RoundlyConsulting\Reviews\Facades\Reviews;
use RoundlyConsulting\Reviews\Models\Review;

it('records the review and the owner reply', function () {
    $fake = Reviews::fake();

    $restaurant = Restaurant::factory()->create();
    [$user, $owner] = User::factory()->count(2)->create();

    $review = $restaurant->addReview($user)->rating(5)->create();
    $review->respond($owner, 'Thanks!');

    $fake->assertReviewCreated();
    $fake->assertReviewCreated(fn (Review $review) => $review->rating === 5);
    $fake->assertReviewResponded(fn (Review $response, Review $parent) => $parent->is($review));
    $fake->assertNothingApproved();
});
```

## Every assertion

| Assert | Opposite | Callback receives |
| --- | --- | --- |
| `assertReviewCreated()` | `assertNothingReviewed()` | the review |
| `assertReviewApproved()` | `assertNothingApproved()` | the review |
| `assertReviewRejected()` | `assertNothingRejected()` | the review |
| `assertReviewUpdated()` | `assertNothingUpdated()` | the review |
| `assertReviewDeleted()` | `assertNothingDeleted()` | the review |
| `assertReviewResponded()` | `assertNothingResponded()` | the response, the parent review |
| `assertReviewVoted()` | `assertNothingVoted()` | the review, the voter |
| `assertReviewVoteRemoved()` | `assertNoVoteRemoved()` | the review, the voter |
| `assertReviewVerified()` | `assertNothingVerified()` | the review |
| `assertReviewUnverified()` | `assertNothingUnverified()` | the review |

```php
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Reviews\Models\Review;

$fake->assertReviewCreated(fn (Review $review) => $review->verified);
$fake->assertReviewApproved(fn (Review $review) => $review->is($pending));
$fake->assertReviewRejected();
$fake->assertReviewUpdated();
$fake->assertReviewDeleted();
$fake->assertReviewResponded(fn (Review $response, Review $parent) => $parent->is($review));
$fake->assertReviewVoted(fn (Review $review, Model $voter) => $voter->is($user));
$fake->assertReviewVoteRemoved(fn (Review $review, Model $voter) => $voter->is($user));
$fake->assertReviewVerified();
$fake->assertReviewUnverified();

// The opposites pass when nothing of that kind was recorded:
$fake->assertNothingReviewed();
$fake->assertNothingApproved();
$fake->assertNothingRejected();
$fake->assertNothingUpdated();
$fake->assertNothingDeleted();
$fake->assertNothingResponded();
$fake->assertNothingVoted();
$fake->assertNoVoteRemoved();
$fake->assertNothingVerified();
$fake->assertNothingUnverified();
```

Every assert\*() takes an optional callback and passes when at least one recorded call returns true. Only calls that change something are recorded: approving an approved review, rejecting a rejected one, verifying a verified one (or unverifying an unverified one) and removing a vote that was never cast are no-ops — no write, no event — so they are not. An update() that changes the verified flag is also recorded as verified or unverified. Because operations really run, keep the package’s migrations loaded (RefreshDatabase).

## Pest expectations

Register the matchers once in tests/Pest.php:

```php
// tests/Pest.php
RoundlyConsulting\Reviews\Testing\ReviewExpectations::register();

// In any test:
expect($product)->toHaveReview();
expect($product)->toHaveReview($author);
expect($product)->toHaveApprovedReview();
```

Both matchers look at top-level reviews only, so owner responses never satisfy them.

## InteractsWithReviews

Mix the trait into a test case for actingAsReviewer() / reviewAs() ergonomics:

```php
use RoundlyConsulting\Reviews\Testing\InteractsWithReviews;

uses(InteractsWithReviews::class);

it('queues a new review for moderation', function () {
    $review = $this->actingAsReviewer(User::factory()->create())
        ->reviewAs(Restaurant::factory()->create(), rating: 4, content: 'Great brunch');

    expect($review->isPending())->toBeTrue();
});
```

## Factories

Both models ship factories with states for every shape of review. They build the configured reviews.model / reviews.vote\_model, so a swapped model gets its own rows:

```php
use RoundlyConsulting\Reviews\Models\Review;
use RoundlyConsulting\Reviews\Models\ReviewVote;

$review = Review::factory()
    ->forReviewable($restaurant)
    ->byAuthor($user)
    ->approved()
    ->verified()
    ->rating(5)
    ->create();

Review::factory()->response($review)->byAuthor($owner)->approved()->create();
Review::factory()->forReviewable($restaurant)->withoutRating()->pending()->create();
Review::factory()->forReviewable($restaurant)->helpfulVotes(12, 3)->create();

ReviewVote::factory()->forReview($review)->byVoter($user)->unhelpful()->create();
```

Factories write rows directly — they skip validation, moderation and events. Use the facade when the behaviour under test depends on those.

## The package’s own suite

```bash
composer test
```

## 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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/reviews-for-laravel.md)
