How Do You Test Code That Uses now()?
Scott Keck-Warren • November 6, 2026
Hook / opening beat
- Open with the test that passes every day except one: a subscription-expiry check that went red on the last day of the month, or the suite that only failed when CI ran after 11pm.
- The developer reaction that makes it worse: re-run the build, it goes green, everyone moves on.
- Name the cause in the intro: the code calls
now()internally, so the test has no way to say what "now" is. - Set the scope: this is the cheapest testing win most PHP teams have not taken.
Why now() Breaks Tests
- The mechanics in two beats: a hidden dependency on global state that changes on its own, once per second.
- CODE: the untestable version.
public function isExpired(): bool
{
return $this->expires_at < now();
}
- Nothing in the method signature says it depends on the clock, so nothing in the test can control it.
- The workarounds developers reach for and why they fail:
sleep()makes suites slow, relative fixtures (now()->addDays(30)) test the code against itself, and skipping the test is how the bug ships.
Freezing the Clock in Laravel
- The practical answer first, because most readers are here for it.
Carbon::setTestNow()/$this->travelTo(): set the clock, run the assertion, and the time is whatever you said.- CODE: a test that pins a date, asserts expired vs. not expired on both sides of the boundary.
travel()->days(31)for the "advance the clock mid-test" case, which reads better than arithmetic on fixtures.- The cleanup trap:
Carbon::setTestNow()with no argument intearDown, or a leaked frozen clock breaks unrelated tests later in the suite. Laravel'stravelBack()handles it; note that plain PHPUnit setups have to do it themselves. - Mention
freezeTime()for the case where elapsed time inside the test is the thing causing flakiness.
Injecting a Clock Instead
- The version that survives leaving the framework: PSR-20
ClockInterface. - CODE: the interface, a constructor-injected clock, and the method rewritten to call
$this->clock->now(). - The test then passes a frozen or fake clock with no global state involved.
- Say the honest tradeoff rather than selling it: this is more ceremony, and it earns its keep in domain code and library code, not in a controller.
- Rule of thumb for the reader: Carbon helpers for application tests, an injected clock for anything with real date logic in it or anything you might extract later.
The Bugs Freezing Time Lets You Catch
- The payoff section. These are tests nobody writes because they are impossible without clock control.
- Daylight saving transitions: the "add 24 hours" vs. "add 1 day" difference, and the hour that happens twice.
- Month-end arithmetic: January 31 plus one month, and what your code should do about it.
- Timezone boundaries: a "today's orders" report that disagrees with a customer in another timezone about when today started.
- Leap day, since a yearly renewal on February 29 has to land somewhere.
- CODE: one of these as a real test, use the DST one, since it is the most surprising.
- Pull from the Taming Time talk material for the specific gotchas rather than inventing them.
Closing beat
- Restate the rule: if a method calls
now()inside itself, the clock is an untested input. - Concrete next step: grep the codebase for
now(),time(),date(), andnew DateTime()outside of tests, and start with whatever handles money or expiration. - Callback to the opening flaky test: it was not flaky, it was correct one day a month and nobody could prove it.
- Note this is the first of the testing posts, so readers know more is coming.
Notes
- Internal links to verify against the
blog/{date|Y}/{-title}route before publish:/blog/2026/taming-time-in-php-best-practices-and-gotchas-phpjs-tek-2026(talk recap, main source material)/blog/2025/taming-time-in-php-best-practices-and-gotchas-longhorn-php-2025/blog/2026/the-importance-of-small-feedback-loops(optional, in the flaky-test framing)
- Companion split: the Taming Time talks cover DateTimeImmutable and date math. This post is about testability only, so do not re-teach DateTimeImmutable. Reference the talk recaps for the date-handling side.
- Verify
travelTo,travel,freezeTime, andtravelBackagainst the Laravel version in use. These helpers were added and renamed over several releases. - Pin the PSR-20 example to
psr/clockand show the real interface, not an invented one. - No mermaid diagram needed. If one is wanted, a timeline showing a frozen clock crossing an expiry boundary works better than a flowchart.
AI notice:
The outline for this post was generated via AI, then I wrote the prose and ran it through Grammarly