---
title: "Testing — Opening Hours for Laravel | Roundly"
description: "Record every write with OpeningHours::fake() and assert syncs, exceptions, deletes and refreshes — or freeze time and test against real rows."
url: https://roundly-consulting.com/open-source/docs/opening-hours-for-laravel/testing
language: en
---

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

[Opening Hours for Laravel](https://roundly-consulting.com/open-source/docs/opening-hours-for-laravel.md)

# Testing

OpeningHours::fake() records every write — facade, injected manager, builder save() and the owner trait alike — without touching the database, firing events or materializing intervals. Each write still runs the real manager’s checks against the database as it is — payload validation, calendar key, persisted owner, limits, foreign ids, expectedRevision, and CalendarNotFoundException for exception writes without a calendar — so a test fails where production would. Reads still hit the database, so a faked write is not visible to the next one:

```php
use RoundlyConsulting\OpeningHours\DataTransferObjects\CalendarData;
use RoundlyConsulting\OpeningHours\DataTransferObjects\ExceptionData;
use RoundlyConsulting\OpeningHours\Facades\OpeningHours;

$fake = OpeningHours::fake();

$this->post("/clinics/{$clinic->id}/holidays", ['date' => '2026-12-24']);

$fake->assertExceptionAdded($clinic, callback: fn (ExceptionData $data) => $data->label === 'Holiday');
$fake->assertNothingSynced();

// The owner trait and the builder are recorded too:
$clinic->setOpeningHours(['week' => ['monday' => ['09:00-17:00']]], 'pickup');
OpeningHours::delete($clinic, force: true);

$fake->assertSynced($clinic, 'pickup', fn (CalendarData $data) => count($data->schedules) === 1);
$fake->assertDeleted($clinic, force: true);
OpeningHours::assertNothingRefreshed();          // every assertion also works statically
```

| Assertion | Passes when | Opposite |
| --- | --- | --- |
| `assertSynced($owner, ?$calendar, ?fn (CalendarData): bool)` | A sync matched — the facade, a builder save(), setOpeningHours() or editOpeningHours()->save(). | `assertNothingSynced()` |
| `assertExceptionAdded($owner, ?$calendar, ?fn (ExceptionData): bool)` | exceptions()->add(), closed() or open() matched. | `assertNoExceptionsAdded()` |
| `assertExceptionRemoved($owner, ?int $ruleId, ?$calendar)` | exceptions()->remove() matched. | `assertNoExceptionsRemoved()` |
| `assertDeleted($owner, ?$calendar, ?bool $force)` | delete() matched — including the owner-delete cascade, recorded as force: true. | `assertNothingDeleted()` |
| `assertRefreshed($owner, ?$calendar)` | refresh() matched. | `assertNothingRefreshed()` |
| — | No write of any kind was recorded. | `assertNothingWritten()` |

A null calendar means the default one; owners match by Model::is(). Every assertion also works statically (OpeningHours::assertSynced($clinic)), and writes() returns the recorded RecordedWrite list — operation, owner, calendar and payload — for custom checks. Under the fake, sync() returns an in-memory OpeningHours, exceptions()->add(), closed() and open() return an unsaved ExceptionRule, and remove() and delete() return true. Where the real manager has nothing to do, the fake answers the same and records nothing: delete() and exceptions()->remove() return false, and refresh() of a missing calendar is a no-op. Revision hooks for direct row edits aren’t faked.

## Against real rows

“Now” comes from Laravel’s Date facade, so freeze time with CarbonImmutable::setTestNow() or travelTo() and assert through the public API:

```php
use Carbon\CarbonImmutable;
use RoundlyConsulting\OpeningHours\OpeningHours;

afterEach(fn () => CarbonImmutable::setTestNow());

it('reports the clinic as open on Monday morning', function (): void {
    CarbonImmutable::setTestNow(CarbonImmutable::parse('2026-09-28 10:00', 'Europe/Bratislava'));

    $clinic = Clinic::factory()->create();
    $clinic->setOpeningHours([
        'timezone' => 'Europe/Bratislava',
        'week' => ['monday' => ['08:00-12:00', '13:00-17:00']],
    ]);

    expect($clinic->openingHours()->isOpen())->toBeTrue()
        ->and($clinic->openingHours()->nextClose()?->format('H:i'))->toBe('12:00');
});

it('previews overnight hours without a database', function (): void {
    $preview = OpeningHours::make(['week' => ['saturday' => ['22:00-03:00']]], 'Europe/Bratislava');

    expect($preview->isOpenAt(CarbonImmutable::parse('2026-10-04 01:00', 'Europe/Bratislava')))->toBeTrue();
});
```

Without the fake, seed hours with OpeningHours::sync() or setOpeningHours() — it runs the same validation as production — or with the model factories in RoundlyConsulting\\OpeningHours\\Database\\Factories: CalendarFactory (forOwner(), timezone(), key()), ScheduleFactory (base(), seasonal()), ScheduleRangeFactory (on(), between()), ExceptionRuleFactory (closed(), on(), yearly()), ExceptionRangeFactory (between()) and IntervalFactory. CalendarFactory without forOwner() and IntervalFactory fake an owner\_id of the configured key\_type — an integer, a UUID or a ULID.

OpeningHours::make() needs no database at all — ideal for unit tests of rendering or slot logic. The package’s own suite runs against SQLite, PostgreSQL and MySQL.

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