Code that calls time() or new DateTime() internally cannot be tested at a chosen moment. The usual workarounds are a sleep() in the test, which makes the suite slow, or an assertion loose enough to pass at any time, which makes it useless.
interface Clock
{
/** @return DateTimeImmutable */
public function now();
}
final class SystemClock implements Clock
{
public function now() { return new DateTimeImmutable(); }
}
final class FrozenClock implements Clock
{
private $at;
public function __construct(DateTimeImmutable $at) { $this->at = $at; }
public function now() { return $this->at; }
}
Once the clock is a dependency, the expiry boundary, the month rollover and the leap day are all ordinary test cases with no waiting involved. The rule that makes it work is that nothing below the entry point may read the current time directly — one stray time() deep in a helper reintroduces the problem, and it will be the one path nobody tested.