---
title: "Testing — Appointments for Laravel | Roundly"
description: "Record every booking write with Appointments::fake() and its assert methods, then use the model factory and example Pest tests for the rest."
url: https://roundly-consulting.com/open-source/docs/appointments-for-laravel/testing
language: en
---

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

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

# Testing

Appointments::fake() swaps in a recording AppointmentsFake — behind the facade and in the container, so an injected AppointmentManager is faked too — and returns it. Operations still run against the database: availability checks, participants() reads and the package events keep working. Every write is recorded, whether it came through the facade, an injected manager, the builder, a for() handle or a model shortcut such as $appointment->cancel():

```php
use RoundlyConsulting\Appointments\Enums\Status;
use RoundlyConsulting\Appointments\Facades\Appointments;
use RoundlyConsulting\Appointments\Models\Appointment;

$fake = Appointments::fake();

$this->post('/bookings', [...]);

$fake->assertScheduled(fn (Appointment $appointment) => $appointment->name === 'Consultation');
$fake->assertTransitioned(fn (Appointment $appointment, Status $to, Status $from) => $to === Status::Cancelled);
$fake->assertRescheduled(fn (Appointment $appointment, $previousStartsAt) => true);
$fake->assertParticipantAdded(fn (Appointment $appointment, $participant, $row) => $participant->is($user));
$fake->assertParticipantRemoved(fn (Appointment $appointment, $participant) => $participant->is($user));

$fake->assertNothingScheduled();
$fake->assertNothingRescheduled();
$fake->assertNothingTransitioned();
$fake->assertNoParticipantAdded();
$fake->assertNoParticipantRemoved();
```

## Every assertion

| Assertion | Callback receives | Recorded by |
| --- | --- | --- |
| `assertScheduled(?Closure) · assertNothingScheduled()` | Appointment | create(), schedule()->create(), createRecurring() — one per occurrence. |
| `assertRescheduled(?Closure) · assertNothingRescheduled()` | Appointment, CarbonImmutable $previousStartsAt | for()->reschedule(). |
| `assertTransitioned(?Closure) · assertNothingTransitioned()` | Appointment, Status $to, Status $from | for()->transition() and its shortcuts, plus the model’s confirm(), cancel(), complete(), decline(), markNoShow() and transitionTo(). |
| `assertParticipantAdded(?Closure) · assertNoParticipantAdded()` | Appointment, Model $participant, Participant $row | for()->participants()->add(). |
| `assertParticipantRemoved(?Closure) · assertNoParticipantRemoved()` | Appointment, Model $participant — a removed row is recorded as its model | for()->participants()->remove(). |

- Every callback is optional — without one, the assertion passes when anything of that kind was recorded.
- Every assertion also works statically on the facade, e.g. Appointments::assertScheduled().
- Participants listed on a new appointment are part of its scheduling; only participants()->add() counts as added.
- Asking for the status an appointment already has is a no-op, so it isn’t recorded as a transition.
- A refused call — one that throws — isn’t recorded. Status changes made by the approvals sync listener aren’t host calls and aren’t recorded either.
- Actions called directly bypass the fake.

```php
Appointments::fake();

Appointments::for($appointment)->cancel();   // or $appointment->cancel() — recorded either way

Appointments::assertTransitioned(fn (Appointment $appointment, Status $to) => $to === Status::Cancelled);
Appointments::assertNothingScheduled();
```

## Model factory

The bundled Appointment model has a factory — a pending appointment starting tomorrow with a 60-minute duration — plus a withStatus() state:

```php
use RoundlyConsulting\Appointments\Enums\Status;
use RoundlyConsulting\Appointments\Models\Appointment;

$appointment = Appointment::factory()->create();          // pending, starts tomorrow, 60 minutes

$confirmed = Appointment::factory()
    ->withStatus(Status::Confirmed)
    ->create(['starts_at' => now()->addWeek()]);
```

## Example tests

Fake only the events you assert on, and exercise the same builder and helpers your app uses:

```php
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Appointments\Enums\Status;
use RoundlyConsulting\Appointments\Events\AppointmentStatusChanged;
use RoundlyConsulting\Appointments\Exceptions\SchedulingConflictException;
use RoundlyConsulting\Appointments\Facades\Appointments;
use RoundlyConsulting\Appointments\Models\Appointment;

it('confirms a booking and announces it', function () {
    Event::fake([AppointmentStatusChanged::class]);

    $appointment = Appointment::factory()->create();
    Appointments::for($appointment)->confirm();

    expect($appointment->status)->toBe(Status::Confirmed);
    Event::assertDispatched(
        AppointmentStatusChanged::class,
        fn (AppointmentStatusChanged $e) => $e->from === Status::Pending && $e->to === Status::Confirmed,
    );
});

it('blocks a double booking', function () {
    $doctor = User::factory()->create();

    Appointments::schedule('First')->startingAt('2026-07-01 09:00')->lasting(60)
        ->withParticipant($doctor)->create();

    expect(fn () => Appointments::schedule('Clash')->startingAt('2026-07-01 09:30')->lasting(60)
        ->withParticipant($doctor)->preventConflicts()->create()
    )->toThrow(SchedulingConflictException::class);
});
```

Run the suite with the published migrations loaded (RefreshDatabase) — the appointments tables plus the provider tables from Installation.

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