Freeze time in PHP tests with Carbon setTestNow
Freeze Carbon::now() in PHP tests with setTestNow, reset it after each test, and avoid date leakage across your suite.
13 Jul 2025, 18:22 UTC

The useful answer
If a test checks whether a subscription expires tomorrow, its result can depend on the wall-clock time and fail at midnight. Carbon's setTestNow freezes the value returned by Carbon::now() and related helpers so date comparisons, scheduling, and expiry calculations stay deterministic during a test run. The practical rule is: set the frozen time in setUp, clear it with setTestNow(null) in tearDown, and never leave it enabled in production code.
How the freeze works
setTestNow stores a Carbon instance in process-global state. That means every part of the same PHP request or test process sees the same frozen value until you clear it. When you call Carbon::setTestNow(null), Carbon returns to reading the real system clock.
Carbon offers both mutable and immutable date objects. A mutable Carbon instance can be changed in place by methods such as addDays. An immutable CarbonImmutable instance returns a new object instead, which is safer when a frozen base time is reused across services or tests. For CarbonImmutable, call CarbonImmutable::setTestNow rather than the mutable class.
Worked configuration
These examples assume Carbon 2.x from nesbot/carbon. Confirm your installed release in composer.lock before adapting the calls.
<?php
require __DIR__ . '/vendor/autoload.php';
use Carbon\CarbonImmutable;
use PHPUnit\Framework\TestCase;
class SubscriptionTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
CarbonImmutable::setTestNow(
CarbonImmutable::parse('2026-01-01 12:00:00')
);
}
protected function tearDown(): void
{
CarbonImmutable::setTestNow(null);
parent::tearDown();
}
public function testTrialExpiresAfterFourteenDays(): void
{
$now = CarbonImmutable::now(); // frozen at 2026-01-01 12:00:00
$expiry = $now->addDays(14); // new instance; $now is unchanged
$this->assertSame(
'2026-01-15 12:00:00',
$expiry->format('Y-m-d H:i:s')
);
}
}
The freeze applies to now(), today(), tomorrow(), and similar helpers. Because CarbonImmutable::addDays returns a new instance, the frozen base time is not accidentally mutated by the assertion.
Limits and common mistakes
- Process-global state. Once set, the freeze affects unrelated code in the same request or test suite. A test that forgets to clear it can shift dates in every later test.
- No parallel isolation.
setTestNowis not thread-safe and does not scope automatically per test in parallel runners. Use explicit setup and teardown in each test class. - Testing only. Do not call
setTestNowin application code, queue workers, or scheduled jobs. It is a testing and seeding tool. - Mutable reuse. If you use mutable
Carboninstead ofCarbonImmutable, passing the frozennow()into multiple services can let one service alter the shared base time. - Timezones and DST. Freezing a timestamp does not remove timezone or daylight-saving behavior. Set an explicit timezone on your Carbon instances or configure
date.timezonewhen testing boundary dates.
Verify the behavior
Create verify_test_now.php in your project root:
<?php
require __DIR__ . '/vendor/autoload.php';
use Carbon\Carbon;
echo 'Before: ' . Carbon::now()->toDateTimeString() . PHP_EOL;
Carbon::setTestNow(Carbon::parse('2026-06-15 08:30:00'));
echo 'Frozen: ' . Carbon::now()->toDateTimeString() . PHP_EOL;
Carbon::setTestNow(null);
echo 'After: ' . Carbon::now()->toDateTimeString() . PHP_EOL;
Run it from the project root with php verify_test_now.php. The file needs read permission; no special privileges are required. The middle line should print the frozen value, and the final line should return to wall-clock time. If the script exits before setTestNow(null), the freeze ends when the CLI process exits, but in a long-running PHP process it would persist until cleared or the process ends.
To detect leakage in PHPUnit, add one test that sets the freeze without clearing it and another test that asserts Carbon::now() is close to the real time. If the second test fails depending on execution order, your teardown is not resetting the global state.
Rollback
Because setTestNow changes process-global state, the rollback is always Carbon::setTestNow(null) or CarbonImmutable::setTestNow(null). Put it in tearDown or a finally block so it runs even when a test fails.
| Call | Effect on Carbon::now() |
|---|---|
| None (default) | Returns actual wall-clock time |
setTestNow($instance) | Returns the frozen instance |
setTestNow(null) | Restores actual wall-clock time |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.