---
title: "Testing — Requests for Laravel | Roundly"
description: "Swap in Requests::fake() to record every call without database writes, or test the real engine with factory states."
url: https://roundly-consulting.com/open-source/docs/requests-for-laravel/testing
language: en
---

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

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

# Testing

Requests::fake() swaps a recording fake in behind the facade and the container and returns it, so the facade, the fluent builder and any constructor-injected RequestManager all land on it. Mutating calls are recorded and never touch the database — no hand-rolled Event::fake() needed; canTransition() still answers for real:

```php
use RoundlyConsulting\Requests\Facades\Requests;

it('submits an expense claim', function () {
    $fake = Requests::fake();
    $alice = User::factory()->create();

    $request = Requests::make()->title('Expense')->create();
    Requests::approve($request, $alice, 'Within budget');

    $fake->assertCreated(1);
    $fake->assertApproved($request, $alice, 'Within budget');   // or Requests::assertApproved(...)
    $fake->assertNothingRejected();
});

it('submits nothing when the form is invalid', function () {
    $fake = Requests::fake();

    // …

    $fake->assertNothingCreated();
});
```

## Every assertion

| Assertion | Passes when |
| --- | --- |
| `assertCreated(?int $times = null)` | At least one request was created — or exactly $times. |
| `assertNothingCreated()` | No request was created. |
| `assertApproved(Request, ?Model $actor, ?string $reason)` | approve() was called for the request — by that actor and with that reason, when given. |
| `assertNothingApproved()` | approve() was never called. |
| `assertRejected(Request, ?Model $actor, ?string $reason)` | The same, for reject(). |
| `assertNothingRejected()` | reject() was never called. |
| `assertReopened(Request, ?Model $actor, ?string $reason)` | The same, for reopen(). |
| `assertNothingReopened()` | reopen() was never called. |
| `assertCancelled(Request)` | cancel() was called for the request. |
| `assertNothingCancelled()` | cancel() was never called. |
| `assertExpired(Request)` | expire() was called for the request. |
| `assertNothingExpired()` | expire() was never called. |
| `assertExpiredDue(?bool $dryRun = null)` | expireDue() ran — with that dry-run flag, when given. |
| `assertNothingExpiredDue()` | expireDue() never ran. |

```php
$fake = Requests::fake();

$fake->assertCreated();                           // at least one — or assertCreated(2) for exactly two
$fake->assertApproved($request);                  // actor and reason are optional filters
$fake->assertRejected($request, $carol, 'Out of policy');
$fake->assertReopened($request, $alice);
$fake->assertCancelled($request);
$fake->assertExpired($request);
$fake->assertExpiredDue();                        // expireDue() ran
$fake->assertExpiredDue(dryRun: true);            // …as a dry run

// Every assertion has a negative twin
$fake->assertNothingCreated();
$fake->assertNothingApproved();
$fake->assertNothingRejected();
$fake->assertNothingReopened();
$fake->assertNothingCancelled();
$fake->assertNothingExpired();
$fake->assertNothingExpiredDue();
```

The asserts are also callable statically on the facade. The fake’s create() returns an unsaved instance of your configured requests.model, built exactly as the real create builds it — author, status, type, title, description, meta, the declared approvers’ keys and the explicit or default\_ttl expiry — and a bare-id approver throws InvalidApprover just the same. approve(), reject() and reopen() record the reason you pass. expireDue() returns how many requests are due without expiring any — so requests:expire run under the fake is recorded too.

Assertions match requests and actors by key. An unsaved request — what the fake’s create() returns — has none, so it matches only the very same instance, and two requests created under the fake are never mistaken for each other.

## Factory states

The Request model ships a factory with a state per status plus helpers for approvers and authors. requiring() stores approver ids only — it opens no approval request:

```php
use RoundlyConsulting\Requests\Models\Request;

Request::factory()->pending()->create();
Request::factory()->approved()->create();
Request::factory()->rejected()->create();
Request::factory()->cancelled()->create();
Request::factory()->expired()->create();                       // New, expires_at yesterday
Request::factory()->requiring([$alice->id, $bob->id])->create();
Request::factory()->authoredBy($user)->create();
```

## Against the real engine

For integration tests, load both the requests and the approvals migrations and drive the real flow:

```php
use RoundlyConsulting\Requests\Enums\Status;
use RoundlyConsulting\Requests\Facades\Requests;
use RoundlyConsulting\Requests\Models\Request;

it('approves once both managers sign off', function () {
    $alice = User::factory()->create();
    $bob = User::factory()->create();

    $request = Requests::make()->requireApprovalsFrom([$alice, $bob])->create();

    Requests::approve($request, $alice);
    expect($request->fresh()->status)->toBe(Status::New);

    Requests::approve($request, $bob);
    expect($request->fresh()->status)->toBe(Status::Approved);
});

it('expires stale requests', function () {
    Request::factory()->expired()->create();

    $this->artisan('requests:expire')
        ->expectsOutput('Expired 1 request(s).')
        ->assertSuccessful();
});
```

The package’s own suite runs with:

```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/requests-for-laravel.md)
