---
title: "Testing — Teams for Laravel | Roundly"
description: "Teams::fake() records every operation while still performing it, with an assertion per operation; Pest matchers and factories keep tests short."
url: https://roundly-consulting.com/open-source/docs/teams-for-laravel/testing
language: en
---

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

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

# Testing

Teams::fake() swaps in RoundlyConsulting\\Teams\\Testing\\TeamsFake — a recording subclass of TeamsManager — behind the facade and the TeamsManager container binding, and returns it. Operations still run against your test database; the fake records each one so you can assert on what your code asked for:

```php
use RoundlyConsulting\Teams\DataTransferObjects\CreateTeamData;
use RoundlyConsulting\Teams\Facades\Teams;

it('creates a team and adds an admin', function () {
    $fake = Teams::fake();   // TeamsFake — also swapped in for an injected TeamsManager
    $owner = User::factory()->create();
    $alice = User::factory()->create();
    $bob = User::factory()->create();

    $team = Teams::create(new CreateTeamData(name: 'Acme', owner: $owner));
    Teams::for($team)->members()->add($alice, 'admin');
    $team->addMember($bob, 'member');   // a model method — recorded too

    $fake->assertTeamCreated('Acme');
    Teams::assertMemberAdded($team, $alice, 'admin');   // the facade forwards to the fake
    Teams::assertMemberAdded($team, $bob);
    Teams::assertNothingRemoved();

    expect($alice)->toBeMemberOf($team);   // the operation really ran
});
```

Every state change funnels through one manager method, so the fake sees calls made through the facade, an injected TeamsManager, every handle, the model methods ($team->addMember(), $invite->revoke(), $membership->removeFromTeam()), the Artisan commands and the approvals listener. Call the assertions on the returned fake or through the facade.

- Only the operation you called is recorded, not what it does internally — accepting an invite records the accept, not the member add inside it.
- A refused call (another team’s invite, a non-member) throws before it runs and is not recorded.
- Registering global roles and permissions is boot-time configuration and is not recorded; Teams::members()->expiring() is a read and isn’t either.
- Calling an action directly bypasses the manager, so the fake doesn’t see it.

## Every assertion

| Operation | Assert | Negative |
| --- | --- | --- |
| `create()` | `assertTeamCreated(?name)` | `assertNothingCreated()` |
| `transferOwnershipTo()` | `assertOwnershipTransferred($team, ?to)` | `assertNothingTransferred()` |
| `members()->add()` | `assertMemberAdded($team, $member, ?role), assertMemberNotAdded($team, $member)` | `assertNothingAdded()` |
| `members()->remove()` | `assertMemberRemoved($team, $member)` | `assertNothingRemoved()` |
| `members()->changeRole()` | `assertRoleChanged($team, $member, ?role)` | `assertNoRoleChanged()` |
| `invites()->create()` | `assertInviteCreated($team, ?email)` | `assertNothingInvited()` |
| `invites()->resend()` | `assertInviteResent($invite)` | `assertNothingResent()` |
| `invites()->revoke()` | `assertInviteRevoked($invite)` | `assertNothingRevoked()` |
| `invites()->accept()` | `assertInviteAccepted(?invite, ?by)` | `assertNothingAccepted()` |
| `joinRequests()->open()` | `assertJoinRequested($team, ?requester)` | `assertNothingRequested()` |
| `joinRequests()->approve()` | `assertJoinRequestApproved($request, ?by)` | `assertNothingApproved()` |
| `joinRequests()->deny()` | `assertJoinRequestDenied($request, ?by)` | `assertNothingDenied()` |
| `roles()->define()` | `assertRoleDefined($team, ?key)` | `assertNothingDefined()` |
| `members()->notifyExpiring()` | `assertExpiringMembersNotified()` | `assertNothingNotified()` |
| `invites()->prune(), members()->prune(), joinRequests()->expire()` | `assertInvitesPruned(), assertMembersPruned(), assertJoinRequestsExpired()` | `assertNothingPruned()` |
| anything | `recorded(?TeamOperation)` | `assertNothingRecorded()` |

```php
use RoundlyConsulting\Teams\Enums\TeamOperation;
use RoundlyConsulting\Teams\Facades\Teams;

// Teams
Teams::assertTeamCreated('Acme');                    // name optional
Teams::assertOwnershipTransferred($team, to: $alice);
Teams::assertNothingCreated();
Teams::assertNothingTransferred();

// Members
Teams::assertMemberAdded($team, $alice, 'admin');    // role optional
Teams::assertMemberNotAdded($team, $carol);
Teams::assertMemberRemoved($team, $bob);
Teams::assertRoleChanged($team, $alice, 'editor');   // role optional
Teams::assertExpiringMembersNotified();
Teams::assertNothingAdded();
Teams::assertNothingRemoved();
Teams::assertNoRoleChanged();
Teams::assertNothingNotified();

// Invites
Teams::assertInviteCreated($team, 'jane@acme.test'); // email optional
Teams::assertInviteResent($invite);
Teams::assertInviteRevoked($invite);
Teams::assertInviteAccepted($invite, by: $user);     // both optional
Teams::assertNothingInvited();
Teams::assertNothingResent();
Teams::assertNothingRevoked();
Teams::assertNothingAccepted();

// Join requests and per-team roles
Teams::assertJoinRequested($team, $user);            // requester optional
Teams::assertJoinRequestApproved($request, by: $admin);
Teams::assertJoinRequestDenied($request, by: $admin);
Teams::assertRoleDefined($team, 'editor');           // key optional
Teams::assertNothingRequested();
Teams::assertNothingApproved();
Teams::assertNothingDenied();
Teams::assertNothingDefined();

// Housekeeping
Teams::assertInvitesPruned();
Teams::assertMembersPruned();
Teams::assertJoinRequestsExpired();
Teams::assertNothingPruned();

// Anything
Teams::recorded();                                   // list<RecordedTeamOperation>
Teams::recorded(TeamOperation::AddMember);           // only one operation
Teams::assertNothingRecorded();
```

## Inspecting recorded operations

recorded() returns RecordedTeamOperation objects — the operation, the call’s context and its result — with helpers to read the context (keys include team, member, role, invite, email, requester, request, by and key):

```php
use RoundlyConsulting\Teams\Enums\TeamOperation;

$op = Teams::recorded(TeamOperation::AddMember)[0];

$op->operation;                  // TeamOperation::AddMember
$op->result;                     // the Member the call returned
$op->model('member');            // ?Model from the call's context
$op->involves('team', $team);    // bool
$op->string('role');             // ?string — 'admin'
```

## Pest matchers

Register three expectation matchers from tests/Pest.php — call TeamExpectations::register() or require the file published by teams-stubs:

```php
// tests/Pest.php
use RoundlyConsulting\Teams\Testing\TeamExpectations;

TeamExpectations::register();

// or, after publishing teams-stubs:
require __DIR__.'/Teams.php';
```

```php
expect($user)
    ->toBeMemberOf($team)
    ->withRole($team, 'admin')
    ->toHaveTeamPermission($team, 'posts.publish');
```

## Factories

Every model has a factory, with states for the lifecycle cases:

```php
use RoundlyConsulting\Teams\Models\Invite;
use RoundlyConsulting\Teams\Models\JoinRequest;
use RoundlyConsulting\Teams\Models\Member;
use RoundlyConsulting\Teams\Models\Team;

Team::factory()->create();
Member::factory()->expired()->create();
Member::factory()->expiringAt(now()->addDays(5))->create();
Invite::factory()->maxUses(5)->forEmail('jane@example.com')->create();
Invite::factory()->expired()->create();
JoinRequest::factory()->expiringWithin(3)->create();
JoinRequest::factory()->expired()->create();
```

Member and join-request factories fill the morph columns with placeholder values — pass member\_type and member\_id (or requester\_type and requester\_id) when you need a real model.

## The package’s own tests

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