How do you make a class that calls time() deterministic in a test?
TrialService::isExpired() reads the wall clock directly, so its result changes with the time of day and a test cannot pin an outcome. Refactor it so a test can control the current time. Constraints: do not freeze or override a global, and do not change the method's public behaviour.
<?php
class TrialService {
public function isExpired(int $startedAt, int $days): bool {
return time() > $startedAt + $days * 86400;
}
}
Write the implementation.
Inject a clock instead of calling the global. Declare a Clock interface with now(): int, take it in the constructor and call $this->clock->now(). Production wires a SystemClock that returns time(); the test passes a frozen clock with a fixed timestamp. No global is touched, so the tests stay isolated and can still run in parallel.
- ✗Freezing or overriding a global clock, leaking state across tests and breaking parallel runs
- ✗Stubbing the class under test itself, so the behaviour being tested never executes
- ✗Reading the clock deep inside the method, leaving no seam where a test can substitute it
- →How does the clock interface standardised by PSR-20 make this seam reusable across libraries?
- →Why does a frozen global clock break a parallel test run?
The problem is not time() itself but that the dependency on the clock is hard-wired inside the method, with nowhere to substitute it. Lift it into a seam — an interface that arrives through the constructor.
<?php
interface Clock {
public function now(): int;
}
final class SystemClock implements Clock {
public function now(): int {
return time();
}
}
final class FrozenClock implements Clock {
public function __construct(private int $at) {}
public function now(): int {
return $this->at;
}
}
final class TrialService {
public function __construct(private Clock $clock) {}
public function isExpired(int $startedAt, int $days): bool {
return $this->clock->now() > $startedAt + $days * 86400;
}
}
The test passes new FrozenClock(1700000000) and pins both edges of the interval. No global is touched, so the tests stay isolated and survive a parallel run. In production the container binds Clock to SystemClock, and the method's public behaviour is unchanged.