A Black Friday SaleWindow that calls new DateTimeImmutable() itself can only be tested on Black Friday. Give it a PSR-20 Psr\Clock\ClockInterface (composer require psr/clock), whose single now() method the test controls (symfony/clock ships a ready MockClock):
<?php
use PHPUnit\Framework\TestCase;
use Psr\Clock\ClockInterface;
use Shop\SaleWindow;
final class SaleWindowTest extends TestCase {
public function testTheSaleClosesAtMidnight(): void {
$clock = new class implements ClockInterface {
public DateTimeImmutable $now;
public function now(): DateTimeImmutable { return $this->now; }
};
$sale = new SaleWindow($clock, new DateTimeImmutable('2026-11-27'),
new DateTimeImmutable('2026-11-28'));
$clock->now = new DateTimeImmutable('2026-11-27 23:59:59');
$this->assertTrue($sale->isOpen());
$clock->now = new DateTimeImmutable('2026-11-28 00:00');
$this->assertFalse($sale->isOpen());
}
}Testing both sides of the boundary catches off-by-one bugs. Randomness takes the same seam through PHP 8.2's Random\Randomizer (Subsection 4.7.6): production passes the default secure engine, and a test passes new Randomizer(new Mt19937(42)), which makes the eight-character coupon generator return G6B9V7LM on every run. For files, let the code take a path and give it a fresh one from tempnam(sys_get_temp_dir(), 'shop') or a directory made in setUp() and removed in tearDown(), then check with assertStringEqualsFile().