---
title: "Testing — Permissions for Laravel | Roundly"
description: "Record every role and permission write with Permissions::fake() and 26 assertions, then test end-to-end with factories and HTTP."
url: https://roundly-consulting.com/open-source/docs/permissions-for-laravel/testing
language: en
---

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

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

# Testing

Permissions::fake() swaps in a recording PermissionsFake and returns it. Everything still runs against the database — grants land and the Gate answers — while every write is recorded, whether it came through the facade, an injected manager, a for() handle, the traits, Role::findOrCreate() / Permission::findOrCreate() or the artisan commands. Names are normalized, so an enum matches its string:

```php
use RoundlyConsulting\Permissions\Facades\Permissions;

$fake = Permissions::fake();

$user->assignRole('editor');
Permissions::syncFrom(PostPermission::class);

$fake->assertRoleAssigned($user, RoleName::Editor);
$fake->assertSyncedFrom(PostPermission::class);
$fake->assertNoPermissionGranted();
```

Because the fake is also bound in the container, a class that injects PermissionsManager is covered too:

```php
use RoundlyConsulting\Permissions\Facades\Permissions;

it('makes new staff editors', function () {
    $fake = Permissions::fake();
    $user = User::factory()->create();

    app(OnboardEditor::class)->handle($user); // injects PermissionsManager

    $fake->assertRoleRegistered('editor');
    $fake->assertRoleAssigned($user, 'editor');

    expect($user->hasRole('editor'))->toBeTrue(); // still performed — the row exists
});
```

## Every assertion

| Assertion | Negative | Recorded from |
| --- | --- | --- |
| `assertRoleRegistered($name)` | `assertNoRoleRegistered()` | role(), Role::findOrCreate() |
| `assertPermissionRegistered($name)` | `assertNoPermissionRegistered()` | permission(), Permission::findOrCreate() |
| `assertSyncedFrom($enum)` | `assertNothingSyncedFrom()` | syncFrom(), syncRolesFrom() |
| `assertRoleAssigned($holder, ?$role)` | `assertNoRoleAssigned()` | assignRole() |
| `assertRoleRemoved($holder, ?$role)` | `assertNoRoleRemoved()` | removeRole() |
| `assertRolesSynced($holder, ?$roles)` | `assertNoRolesSynced()` | syncRoles() — exact set |
| `assertPermissionGranted($holder, ?$permission)` | `assertNoPermissionGranted()` | givePermissionTo() |
| `assertPermissionRevoked($holder, ?$permission)` | `assertNoPermissionRevoked()` | revokePermissionTo() |
| `assertPermissionsSynced($holder, ?$permissions)` | `assertNoPermissionsSynced()` | syncPermissions() — exact set |
| `assertAuthorizationForgotten($holder)` | `assertNoAuthorizationForgotten()` | forgetAllAuthorization() |
| `assertOrphansPruned()` | `assertNoOrphansPruned()` | pruneOrphans(), permissions:prune-orphans |
| `assertCacheForgotten()` | `assertCacheNotForgotten()` | cache()->forget(), permissions:cache-reset |
| `assertMemoFlushed()` | `assertMemoNotFlushed()` | cache()->flushMemo() |

The holder-scoped assertions take the role or permission as an optional second argument; the synced ones compare the exact set. Every negative takes no arguments:

```php
// Catalog
$fake->assertRoleRegistered('editor');                // role(), Role::findOrCreate()
$fake->assertPermissionRegistered('posts.edit');      // permission(), Permission::findOrCreate()
$fake->assertSyncedFrom(PostPermission::class);       // syncFrom(), syncRolesFrom()

// Roles on a holder — the role argument is optional
$fake->assertRoleAssigned($user);                     // any role
$fake->assertRoleAssigned($user, RoleName::Editor);   // this role
$fake->assertRoleRemoved($user, 'administrator');
$fake->assertRolesSynced($user, ['editor']);          // exactly this set

// Direct permissions on a holder or a role
$fake->assertPermissionGranted($role, 'posts.edit');
$fake->assertPermissionRevoked($role, 'posts.edit');
$fake->assertPermissionsSynced($user, [PostPermission::Edit]); // exactly this set

// Cleanup and cache
$fake->assertAuthorizationForgotten($user);
$fake->assertOrphansPruned();
$fake->assertCacheForgotten();
$fake->assertMemoFlushed();

// The negatives take no arguments
$fake->assertNoRoleRegistered();
$fake->assertNoPermissionRegistered();
$fake->assertNothingSyncedFrom();
$fake->assertNoRoleAssigned();
$fake->assertNoRoleRemoved();
$fake->assertNoRolesSynced();
$fake->assertNoPermissionGranted();
$fake->assertNoPermissionRevoked();
$fake->assertNoPermissionsSynced();
$fake->assertNoAuthorizationForgotten();
$fake->assertNoOrphansPruned();
$fake->assertCacheNotForgotten();
$fake->assertMemoNotFlushed();
```

A refused write — an unknown name or the wrong holder — is not recorded. The package’s own cache housekeeping is not recorded either: the invalidation after every grant and the memo reset per job or request. The cache assertions see only explicit Permissions::cache() calls and permissions:cache-reset.

## End-to-end tests

Without the fake, test against a real database with RefreshDatabase — the migrations you published run like any other. Grant through the facade, then assert through the model or through HTTP:

```php
use RoundlyConsulting\Permissions\Facades\Permissions;

it('lets editors view users', function () {
    Permissions::permission('auth.users.view');
    Permissions::for(Permissions::role('editor'))->givePermissionTo('auth.users.view');

    $user = User::factory()->create();
    Permissions::for($user)->assignRole('editor');

    expect($user->hasPermissionTo('auth.users.view'))->toBeTrue()
        ->and($user->can('auth.users.view'))->toBeTrue();

    $this->actingAs($user)->get('/users')->assertOk();
});

it('forbids users without the permission', function () {
    Permissions::permission('auth.users.view');

    $this->actingAs(User::factory()->create())
        ->get('/users')
        ->assertForbidden();
});
```

## Factories

Role and Permission ship with factories; names default to a unique role-… or permission-… slug:

```php
use RoundlyConsulting\Permissions\Facades\Permissions;
use RoundlyConsulting\Permissions\Models\Permission;
use RoundlyConsulting\Permissions\Models\Role;

$role = Role::factory()->create();                                // name: "role-…"
$permission = Permission::factory()->create(['name' => 'reports.export']);

Permissions::for($role)->givePermissionTo($permission);
```

## Cache in tests

Every Eloquent write invalidates the catalog, so tests need no manual flushing. If a test seeds permissions with DB::table() or insert(), call Permissions::cache()->forget() afterwards.

## The package’s own suite

To run the package’s test suite from a checkout:

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