Как сделать класс, вызывающий time(), детерминированным в тесте?
TrialService::isExpired() читает системные часы напрямую, поэтому результат зависит от времени суток и тест не может зафиксировать исход. Отрефакторьте так, чтобы тест управлял текущим временем. Ограничения: не замораживайте и не подменяйте глобальную функцию и не меняйте публичное поведение метода.
<?php
class TrialService {
public function isExpired(int $startedAt, int $days): bool {
return time() > $startedAt + $days * 86400;
}
}
Допишите реализацию.
Внедрите часы вместо вызова глобальной функции. Объявите интерфейс Clock с методом now(): int, примите его в конструкторе и зовите $this->clock->now(). В проде подставляется SystemClock, возвращающий time(); в тест передаётся замороженный clock с фиксированной меткой. Глобальное состояние не трогается, поэтому тесты остаются изолированными и могут идти параллельно.
- ✗Замораживать или подменять глобальные часы, протекая состоянием между тестами и ломая параллельный прогон
- ✗Стабить сам тестируемый класс, из-за чего проверяемое поведение вообще не выполняется
- ✗Читать часы в глубине метода, не оставляя шва, где тест мог бы их подменить
- →Как интерфейс часов, стандартизованный в PSR-20, делает этот шов переиспользуемым между библиотеками?
- →Почему замороженные глобальные часы ломают параллельный прогон тестов?
Проблема не в time() как таковой, а в том, что зависимость от часов зашита внутрь метода и её негде подменить. Выносим её в шов — интерфейс, который приходит через конструктор.
<?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;
}
}
В тесте передаём new FrozenClock(1700000000) и проверяем оба края интервала. Глобальное состояние не трогается, поэтому тесты изолированы и переживают параллельный прогон. В проде контейнер связывает Clock с SystemClock, и публичное поведение метода не меняется.