---
title: "Testing — Connections for Laravel | Roundly"
description: "Record every write with Connections::fake() — facade, injected manager and trait — assert each operation, and seed rows with the factory."
url: https://roundly-consulting.com/open-source/docs/connections-for-laravel/testing
language: en
---

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

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

# Testing

Connections::fake() swaps the manager for a recording fake — in the facade and in the container, so injected ConnectionManagers, the permissions() sub-accessor and every HasConnections trait write are recorded too. Operations still hit your test database (so reads work) and are recorded once they succeed — a verb that throws records nothing — in the spirit of Bus::fake():

```php
use RoundlyConsulting\Connections\Facades\Connections;

it('invites a member to a team', function () {
    $user = User::factory()->create();
    $team = Team::factory()->create();
    $otherTeam = Team::factory()->create();

    $fake = Connections::fake();

    Connections::between($user, $team)->withPermissions('view')->invite();
    $team->acceptConnectionFrom($user);                            // trait call — recorded too
    Connections::between($user, $team)->permissions()->grant('publish');

    $fake->assertInvited($user, $team);
    $fake->assertAccepted($user, $team);
    $fake->assertConnected($user, $team);                          // connect() or invite() recorded
    $fake->assertConnectedTimes(1);
    $fake->assertGranted($user, $team, 'publish');
    $fake->assertHasPermissionThrough($user, $team, 'view');
    $fake->assertNotConnected($user, $otherTeam);
    $fake->assertNothingDisconnected();

    // Pass-through: the rows were really written.
    expect($user->hasPermissionThroughConnection($team, 'publish'))->toBeTrue();
});
```

## Every assertion

Each write has a positive assertion and an assertNothing\* counterpart:

| Operation | Assert | Negative |
| --- | --- | --- |
| `connect() / connectTo() / connectAll() (per target)` | `assertConnected($a, $b) · assertConnectedTimes($n)` | `assertNotConnected($a, $b) · assertNothingConnected()` |
| `invite() / inviteConnection()` | `assertInvited($a, $b)` | `assertNothingInvited()` |
| `accept() / acceptConnectionFrom()` | `assertAccepted($a, $b)` | `assertNothingAccepted()` |
| `block() / blockConnectionFrom()` | `assertBlocked($a, $b)` | `assertNothingBlocked()` |
| `disconnect() / disconnectFrom() / disconnectAll()` | `assertDisconnected($a, $b)` | `assertNothingDisconnected()` |
| `reconnect() / restore() / reconnectTo()` | `assertReconnected($a, $b)` | `assertNothingReconnected()` |
| `extend()` | `assertExtended($a, $b)` | `assertNothingExtended()` |
| `permissions()->grant() / grantAll() / grantThroughConnection()` | `assertGranted($a, $b, ?$permission)` | `assertNothingGranted()` |
| `permissions()->revoke() / revokeAll() / revokeThroughConnection()` | `assertRevoked($a, $b, ?$permission)` | `assertNothingRevoked()` |
| `permissions()->sync() / clear() / syncConnectionPermissions()` | `assertPermissionsSynced($a, $b, ?$set) · assertPermissionsCleared($a, $b)` | `assertNothingPermissionsSynced()` |
| `from()->sync() / syncConnections()` | `assertSynced($connector, ?$set)` | `assertNothingSynced()` |
| `prune() / connections:prune` | `assertPruned()` | `assertNothingPruned()` |
| anything | `assertHasPermissionThrough($a, $b, $permission)` | `assertNothingRecorded()` |

A connect() or connectAll() that leaves a pending invitation records invite, like invite() does — when asPending() was staged, or nothing was staged and a new pair took a pending default\_status. A connect that only updates an existing row records connect.

toggle() records the connect() or disconnect() it performs. Actions you resolve and call directly bypass the manager, so they run normally but aren’t recorded. $fake->recorded() returns the raw list of RecordedOperation objects:

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

Connections::from($user)->sync([$teamA, $teamB]);
Connections::between($user, $teamA)->extend(now()->addYear());
Connections::between($user, $teamA)->permissions()->sync('view', 'edit');
Connections::prune();

$fake->assertSynced($user, [$teamA, $teamB]);                        // the exact desired set
$fake->assertExtended($user, $teamA);
$fake->assertPermissionsSynced($user, $teamA, ['view', 'edit']);
$fake->assertPruned();
$fake->assertNothingRevoked();

$fake->recorded();   // list<RecordedOperation> — the raw log
```

## Factory

The Connection model ships a factory with pending(), blocked() and expired() states — expired() sets expires\_at to a day ago. Supply the morph keys of real models:

```php
use RoundlyConsulting\Connections\Models\Connection;

$pair = [
    'connector_type' => $user->getMorphClass(),
    'connector_id' => $user->getKey(),
    'connectable_type' => $team->getMorphClass(),
    'connectable_id' => $team->getKey(),
];

Connection::factory()->pending()->create([...$pair, 'permissions' => ['view']]);   // an open invitation

// Without a state: accepted, no permissions, no expiry.
// Other states: ->blocked() (grants nothing), ->expired() (expires_at a day ago).
```

## Asserting events

```php
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Connections\Events\ConnectionInvited;
use RoundlyConsulting\Connections\Facades\Connections;

it('dispatches an invitation event', function () {
    Event::fake([ConnectionInvited::class]);

    Connections::between($user, $team)->invite();

    Event::assertDispatched(ConnectionInvited::class);
});
```

Run your suite with the published migration (for example with RefreshDatabase). If a test changes a connection row directly between two permission checks, pass force: true or call Connections::flushCache() — the in-request cache only tracks writes made through the package.

## 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/connections-for-laravel.md)
