PHPUnit gibt nach dem Ausführen einer Test-Suite Informationen über den Ressourcenverbrauch aus. Aber wie misst PHPUnit die vergangene Zeit?
Der alte Ansatz
In der Vergangenheit hat PHPUnit, genauer gesagt die phpunit/php-timer-Komponente, von der es abhängt, microtime(true) zu dem Zeitpunkt aufgerufen, an dem die unten gezeigte Ausgabe erscheint.
Time: 00:02.204, Memory: 26.00 MB
microtime(true) gibt die Anzahl der Sekunden seit der Unix-Epoche mit Mikrosekunden-Genauigkeit zurück. Da PHP die super-globale Variable $_SERVER['REQUEST_TIME_FLOAT'] zu Beginn der Skriptausführung auf die Anzahl der Sekunden seit der Unix-Epoche mit Mikrosekunden-Genauigkeit setzt, kannst du microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'] verwenden, um die Zeit seit dem Start der Skriptausführung zu ermitteln.
Probleme mit globalem Zustand
Im Laufe der Jahre gab es immer wieder Fehlerberichte, dass PHPUnit am Ende entweder keine oder eine falsche vergangene Zeit anzeigt. Oft gab PHP folgende Fehlermeldung aus:
PHP Notice: Undefined index: REQUEST_TIME_FLOAT
Der Code von phpunit/php-timer versucht, auf die Variable $_SERVER['REQUEST_TIME_FLOAT'] zuzugreifen. Da diese jedoch nicht gesetzt ist, tritt der oben gezeigte Fehler auf.
Nur weil $_SERVER['REQUEST_TIME_FLOAT'] von PHP zu Beginn der Skriptausführung gesetzt wird, bedeutet das nicht, dass sie unveränderlich ist. Nichts hindert den Test-Code oder den Produktions-Code daran, Anweisungen wie unset($_SERVER['REQUEST_TIME_FLOAT']); zu verwenden, um die Variable zu löschen.
Wenn eine solche Anweisung wirklich notwendig ist, kannst du diesen Test mit @backupGlobals enabled annotieren. Diese Annotation weist PHPUnit an, vor der Ausführung des Tests eine Kopie aller globalen und super-globalen Variablen zu erstellen und diese Sicherung anschließend zur Wiederherstellung der ursprünglichen Werte zu verwenden.
Vor einiger Zeit habe ich den Code von phpunit/php-timer so geändert, dass eine Exception ausgelöst wird, wenn $_SERVER['REQUEST_TIME_FLOAT'] nicht gesetzt ist. Das löst zwar das eigentliche Problem nicht, ist aber zumindest sauberer als der PHP-Hinweis, der zuvor ausgelöst wurde. Natürlich erhielt ich danach Fehlerberichte, dass PHPUnit diese Exception am Ende wirft.
In letzter Zeit gab es Fehlerberichte, die dadurch verursacht wurden, dass $_SERVER['REQUEST_TIME_FLOAT'] keinen Wert vom Typ float enthält. Das passiert, wenn entweder der Test-Code oder der Produktions-Code $_SERVER['REQUEST_TIME_FLOAT'] auf einen ungültigen Wert setzen. Ich habe daher den Code von phpunit/php-timer erweitert, um eine Exception auszulösen, wenn $_SERVER['REQUEST_TIME_FLOAT'] einen Wert enthält, der nicht vom Typ float ist.
Aber was passiert, wenn Test-Code oder Produktions-Code $_SERVER['REQUEST_TIME_FLOAT'] auf einen Wert setzen, der den richtigen Typ (float) hat, aber nicht die tatsächliche aktuelle Zeit repräsentiert? phpunit/php-timer kann das nicht erkennen und berechnet die vergangene Zeit falsch. Dasselbe gilt, wenn Test-Code oder Produktions-Code die tatsächliche Systemzeit ändern.
Es war an der Zeit zu erkennen, dass „auf die Uhr schauen", insbesondere über eine super-globale Variable wie $_SERVER['REQUEST_TIME_FLOAT'], keine gute Idee ist. Übrigens gilt das nicht nur für die Anzeige der vergangenen Zeit am Ende der Ausführung der Test-Suite, sondern auch für die Messung, wie lange einzelne Tests dauern. Diese Informationen werden beispielsweise in den XML-Logdateien ausgegeben.
hrtime()
Wenn wir also weder $_SERVER['REQUEST_TIME_FLOAT'] noch microtime() vertrauen können, wie sollen wir dann die Zeit messen? Die Antwort lautet hrtime().
hrtime() wurde in PHP 7.3 eingeführt. Die Funktion gibt den aktuellen Wert eines hochauflösenden monotonen Timers zurück, der sich immer vorwärtsbewegt und weder von Systemuhranpassungen noch von Änderungen an super-globalen Variablen wie $_SERVER['REQUEST_TIME_FLOAT'] beeinflusst wird.
Die aktuelle Version von PHPUnit setzt PHP 7.3 voraus: perfekt! Ich darf hrtime() verwenden.
PHPUnit 9.2
PHPUnit 9.2, das heute veröffentlicht wurde, verwendet nun phpunit/php-timer in Version 4.0, das von Grund auf neu geschrieben wurde. PHPUnit verwendet weder $_SERVER['REQUEST_TIME_FLOAT'] noch microtime().
Stattdessen wird hrtime() einmal aufgerufen, wenn der CLI-Test-Runner startet. Am Ende des Testlaufs wird hrtime() erneut aufgerufen. Die Differenz zwischen den beiden Rückgabewerten von hrtime() ist die vergangene Zeit, die ausgegeben wird. Vor und nach jedem Test wird hrtime() aufgerufen. Die Differenz zwischen diesen beiden Rückgabewerten von hrtime() ist die Dauer des jeweiligen Tests.