Laravel Magazine
Testing Time-Dependent Code with Carbon::setTestNow(), travel(), and freezeTime()

Testing Time-Dependent Code with Carbon::setTestNow(), travel(), and freezeTime()

Eric Van Johnson ·

Any test that touches now(), a trial expiration, or a rate-limit window has the same problem: the clock keeps moving while the test runs, and "assert this trial is expired" needs a reliable way to make that true without a sleep() call. Laravel exposes Carbon's test-time helpers directly on the base TestCase, and they're worth knowing well before you reach for manually swapping dates in factories.

travelTo(): pin the clock to an exact moment

use Illuminate\Support\Carbon;

it('marks a trial as expired after 14 days', function () {
    $account = Account::factory()->create(['trial_started_at' => now()]);

    $this->travelTo(now()->addDays(15));

    expect($account->fresh()->onTrial())->toBeFalse();
});

travelTo() sets Carbon::setTestNow() under the hood, so every call to now(), Carbon::now(), and today() anywhere in the application — not just in the test file — returns the frozen instant until it's reset.

travel(): relative jumps

it('sends a renewal reminder 3 days before the subscription ends', function () {
    $subscription = Subscription::factory()->create(['ends_at' => now()->addDays(3)]);

    $this->travel(3)->days();

    Artisan::call('subscriptions:send-renewal-reminders');

    Mail::assertSent(RenewalReminder::class);
});

travel() reads naturally for relative jumps: $this->travel(1)->day(), $this->travel(30)->minutes(), $this->travel(-1)->week() to move backward. Chain ->days() or ->minutes() at the end to specify the unit.

freezeTime() and travelBack()

Sometimes you don't want to move to a different moment — you want to stop time entirely so that two calls to now() a few milliseconds apart in the same test return an identical value, avoiding flaky off-by-a-millisecond assertions:

it('stamps the exact same processed_at for a batch of jobs', function () {
    $this->freezeTime();

    ProcessOrderBatch::dispatchSync($orders);

    expect($orders->fresh()->pluck('processed_at')->unique())->toHaveCount(1);
});

Always call $this->travelBack() when a test is done manipulating time, or rely on Laravel resetting it automatically between tests via the RefreshDatabase/base TestCase teardown — forgetting this is the classic cause of "this test passes alone but fails in the full suite" bugs, because a frozen clock from one test leaks into the next.

afterEach(function () {
    $this->travelBack();
});

Why not just fake the value in a factory?

You could write Account::factory()->create(['trial_started_at' => now()->subDays(15)]) and skip time travel entirely — and for a single, isolated assertion, that's often simpler. Reach for travelTo()/travel() instead when the code under test calls now() in multiple places that all need to agree (a scheduled command, a chain of jobs, a model event listener), or when you're testing the passage of time itself, like a scheduler command that should only fire once per day. Faking one field gets you a stale timestamp; freezing the clock gets you a consistent world.

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.