Introduction
Time-dependent code is notoriously hard to test. Does the password reset token expire after an hour? Does the trial flip to 'expired' at midnight UTC? Does the reminder email go out 24 hours after signup? Laravel's time-travel helpers give you deterministic control over "now" so these tests stop being flaky.
Key Concepts
travelTo($datetime): Fixnow()at an exact moment until you calltravelBack().travel($amount)->days(): Advance the clock by a relative amount (days/hours/minutes).freezeTime(): Locknow()to the current instant so everynow()call inside the test returns the same microsecond.
Real World Context
A trial-expiry job runs when the user signed up exactly 14 days ago. Without time travel you'd either (a) write a test that sleeps for 14 days, or (b) inject a fake clock into every class. Laravel's helpers let you skip both.
Deep Dive
Fixing a specific moment
phpuse Illuminate\Support\Carbon; public function test_trial_expires_after_14_days(): void { $this->travelTo(Carbon::parse('2026-04-14 12:00:00')); $user = User::factory()->create(); $this->assertFalse($user->trialExpired()); $this->travel(14)->days(); $this->assertTrue($user->fresh()->trialExpired()); }
travelTo sets a fixed "now"; travel(14)->days() then moves it forward relative to that fixed moment. Every now(), Carbon::now(), today(), and today()->addDays(n) in your code sees the shifted time.
Freezing time for sub-second assertions
phppublic function test_token_is_stored_with_current_timestamp(): void { $this->freezeTime(); $token = PasswordResetToken::generate('user@example.com'); $this->assertEquals(now(), $token->created_at); }
Without freezeTime(), the test would be flaky: now() when the assertion runs is a microsecond later than now() inside generate().
Using a closure for a scoped travel
php$this->travelTo(now()->addHours(2), function () use ($user) { $this->assertTrue($user->sessionExpired()); }); // Back to the original 'now' automatically $this->assertFalse($user->sessionExpired());
The closure form auto-reverts — you don't need to remember travelBack().
Common Pitfalls
- Forgetting
travelBack()in non-closure tests — the fake time leaks into the next test, causing spooky cross-test failures. Prefer the closure form where possible, or usefreezeTime()inside a setUp that has a matchingtearDown. - Calling
time()directly — the PHP built-in ignores Laravel's time travel. Always usenow()orCarbon::now().
Best Practices
- Freeze time at the top of any test that asserts on timestamps — eliminates entire categories of flakes.
- Use real calendar dates, not
now()->addYears(100)— human-readable dates help when a test fails years from now.
Summary
travelTo(datetime)fixes now at an absolute moment.travel(n)->days()advances the fixed moment.freezeTime()is the micro-level version for timestamp equality.- The closure form of
travelToauto-reverts, so prefer it.