Ein blau beleuchtetes Cockpit-Instrumentenbrett. Anzeigen, Schubhebel und hinterleuchtete Skalen zeigen Geschwindigkeit und Status. Eine Metapher dafür, die Telemetrie zu lesen, die deine Testsuite bei jedem Lauf aufzeichnet.

In „Turbo-Charging Your PHPUnit Suite“ habe ich ein kleines Versprechen gegeben: Bevor du auch nur eine einzige Zeile einer langsamen Testsuite änderst, solltest du messen. Ein kleines, selbstgeschriebenes Skript genügt, um PHPUnits Open-Test-Reporting-Logdatei zu parsen und die langsamsten Tests sichtbar zu machen.

Ich habe dieses Skript geschrieben. Dann ist es erwachsen geworden. Es ist inzwischen ein Werkzeug namens otr-report, und in diesem Artikel geht es darum, was dieses Werkzeug tut und warum sich die Installation meiner Meinung nach lohnt.

Die Daten sind längst da

Jedes Mal, wenn PHPUnit deine Tests ausführt, weiß es vieles, was es dir nie zeigt. Es weiß, wie lange jeder einzelne Test gedauert hat, wie viel CPU-Zeit er verbraucht hat, wie viel Speicher er belegt hat, wie viele Assertions er ausgeführt hat und, falls er gescheitert ist, ganz genau warum. Standardmäßig verpufft fast all dieses Wissen in dem Moment, in dem der Lauf endet und die Zusammenfassungszeile vorbeiscrollt.

Mit Open Test Reporting (OTR) geht dieses Wissen nicht verloren. OTR ist ein strukturiertes XML-Format für Testergebnisse, das sprach- und werkzeugunabhängig konzipiert ist. PHPUnit kann für jeden Lauf eine OTR-Logdatei schreiben, mit der Option --log-otr:

$ phpunit --log-otr /tmp/otr/run.xml

PHPUnit kann OTR-Logdateien seit Version 12.2 schreiben. Was sich mit PHPUnit 13.2 geändert hat, ist, wie viel es aufzeichnet. Das Format enthält nun noch mehr Informationen, die spezifisch für PHP und PHPUnit sind: den Ressourcenverbrauch pro Test und pro Testsuite (Wall-Clock-Zeit, CPU-Zeit und Peak-Speicherverbrauch), die TestDox-Namen deiner Klassen und Methoden, die Anzahl der Assertions, die jeder Test ausgeführt hat, und strukturierte Fehlerdetails mit erwartetem Wert, tatsächlichem Wert und Diff.

OTR selbst ist generisch, aber die Logdatei, die PHPUnit 13.2 erzeugt, enthält mehr Details, als das generische Schema verlangt. otr-report ist das Werkzeug, das diese Details wieder ausliest und in etwas verwandelt, mit dem du arbeiten kannst. PHPUnits eigene Ausgabe ersetzt es nicht. Dafür beantwortet es Fragen, für die diese Ausgabe nie gedacht war.

Welche Tests langsam sind

Die erste Frage, und die, mit der das ganze Projekt begann, ist einfach: Welche Tests fressen meine Zeit?

Der Befehl slowest liest die Logdatei eines einzelnen Laufs und gibt die langsamsten Tests aus, geordnet vom langsamsten zum schnellsten:

$ otr-report slowest /tmp/otr/run.xml
otr-report 1.0.1 by Sebastian Bergmann.

Time(s)   Test
-------   ----
4.441529  SebastianBergmann\Raytracer\PuttingItTogetherTest::test_chapter_8
3.771325  SebastianBergmann\Raytracer\PuttingItTogetherTest::test_chapter_6
1.375265  SebastianBergmann\Raytracer\PuttingItTogetherTest::test_chapter_5
0.845473  SebastianBergmann\Raytracer\PuttingItTogetherTest::test_chapter_10
0.039408  SebastianBergmann\Raytracer\PuttingItTogetherTest::test_chapter_4
...

Das ist fast immer ehrlicher als die Intuition. Wie ich im früheren Artikel dargelegt habe: Die Intuition lügt, und der langsamste Test ist selten der, den das Team erwartet.

Eine schlichte Liste der zehn langsamsten Tests ist ein guter Anfang, aber sie sagt dir nicht, ob diese zehn Ausreißer sind oder einfach die Spitze einer durchgängig langsamen Suite. Die Option --above-mean beantwortet das. Sie berechnet die mittlere Laufzeit über alle Tests und listet dann nur die Tests auf, die langsamer als der Mittelwert sind, jeweils mit dem Faktor versehen, um den sie darüber liegen:

$ otr-report slowest --above-mean /tmp/otr/run.xml
otr-report 1.0.1 by Sebastian Bergmann.

Mean test runtime: 0.059520 s (177 tests, 4 slower than mean)

Time(s)   x mean  Test
-------   ------  ----
4.441529  74.62x  SebastianBergmann\Raytracer\PuttingItTogetherTest::test_chapter_8
3.771325  63.36x  SebastianBergmann\Raytracer\PuttingItTogetherTest::test_chapter_6
1.375265  23.11x  SebastianBergmann\Raytracer\PuttingItTogetherTest::test_chapter_5
0.845473  14.20x  SebastianBergmann\Raytracer\PuttingItTogetherTest::test_chapter_10

Das ist eine ganz andere Diagnose als „die Suite ist langsam“: vier Tests von 177, jeder zwischen vierzehn- und fünfundsiebzigmal langsamer als der Durchschnitt. Sie sagt dir genau, wo sich ein halber Tag konzentrierter Arbeit auszahlt, und welche 173 Tests du getrost in Ruhe lassen kannst.

Die Wall-Clock-Zeit ist nicht das Einzige, was zählt. Ein Test kann schnell sein und trotzdem Hunderte Megabyte belegen, und Speicherdruck ist eine eigene Form von Langsamkeit, sobald parallele Prozesse um den Speicher konkurrieren. Die Option --sort erlaubt es dir, nach time (der Voreinstellung), cpu (User- plus System-CPU-Zeit) oder memory (Peak-Speicherverbrauch) zu sortieren:

$ otr-report slowest --sort memory --limit 3 /tmp/otr/run.xml
otr-report 1.0.1 by Sebastian Bergmann.

Memory    Test
-------   ----
23489616  SebastianBergmann\Raytracer\PuttingItTogetherTest::test_chapter_6
22907872  SebastianBergmann\Raytracer\PuttingItTogetherTest::test_chapter_4
22044384  SebastianBergmann\Raytracer\PuttingItTogetherTest::test_chapter_5

Die Option --limit, oben gezeigt, steuert, wie viele Tests aufgelistet werden; sie ist auf zehn voreingestellt und lässt sich mit jeder anderen Option kombinieren.

Vom Was zum Warum

otr-report slowest sagt dir, welche Tests langsam sind. Es sagt dir nicht, warum, und diese Grenze ist wichtig.

Eine Rangliste ist der Ausgangspunkt einer Untersuchung, nicht ihr Ende. Wenn du weißt, welcher Test die Laufzeit dominiert, aber nicht, was er mit dieser Zeit anstellt, ist das der Moment, zu den Disziplinen zu greifen, die ich in „Debugging von Performance in PHP“ beschreibe: Tracing, um zu sehen, was passiert ist, Profiling, um zu sehen, was am meisten gekostet hat, und Benchmarking, um zu beweisen, dass deine Korrektur tatsächlich geholfen hat.

Die beiden Werkzeuge ergänzen einander. otr-report grenzt eine Suite aus Tausenden Tests auf die Handvoll ein, für die sich ein Profiling lohnt, sodass du deinen Profiler auf den richtigen Code richtest statt auf den ganzen Lauf. Der Profiler sagt dir dann, ob dieser langsame Test ein Datenbankschema neu aufbaut, ein Framework hochfährt oder in einem Helfer drei Schichten tief schläft.

Besser oder schlechter

Ein einzelner Lauf ist eine Momentaufnahme. Eine weitere interessante Frage über die Lebensdauer eines Projekts hinweg ist der Trend. Wird die Suite langsamer, ein harmlos aussehender Commit nach dem anderen, oder hat deine Optimierungsarbeit Bestand?

Der Befehl trends liest jede OTR-Logdatei in einem Verzeichnis und erzeugt einen einzelnen, in sich geschlossenen HTML-Report. Der natürliche Weg, ihn zu füttern, besteht darin, pro Continuous-Integration-Lauf eine Logdatei in ein gemeinsames Verzeichnis zu archivieren:

$ phpunit --log-otr /tmp/otr/2026-02-02.xml
$ phpunit --log-otr /tmp/otr/2026-02-09.xml
$ phpunit --log-otr /tmp/otr/2026-02-16.xml

$ otr-report trends /tmp/otr /tmp/trends.html
otr-report 1.0.1 by Sebastian Bergmann.

Wrote trends report to /tmp/trends.html

Der Report stellt die Gesamtlaufzeit der Suite und die Anzahl der Tests über alle Läufe hinweg grafisch dar, geordnet danach, wann jeder Lauf gestartet wurde. Außerdem listet er die zehn langsamsten Tests des jüngsten Laufs auf, jeweils mit einer Sparkline seiner Laufzeit über alle aufgezeichneten Läufe hinweg und der Veränderung gegenüber dem ersten Mal, als dieser Test gemessen wurde. Ein Test, dessen Laufzeit sich über drei Monate langsam verdreifacht hat, ist genau die Art von Drift, die kein einzelner Lauf je offenbart, die dir ein Trend-Report aber sofort zeigt.

Das ganze Bild

Performance war der Auslöser für dieses Werkzeug, aber die OTR-Logdatei zeichnet das Ergebnis jedes Tests auf, nicht nur seine Laufzeit. Der Befehl results verwandelt einen einzelnen Lauf in einen in sich geschlossenen HTML-Report, der das ganze Bild zeigt:

$ otr-report results /tmp/otr/run.xml /tmp/results.html
otr-report 1.0.1 by Sebastian Bergmann.

Wrote test results report to /tmp/results.html

Der Report beginnt mit einer Zusammenfassung des Laufs und gibt dir eine fixierte Seitenleiste mit einem aufklappbaren Baum, der die Tests nach Namespace, dann Klasse, dann Methode gruppiert, mit einer Statusanzeige auf jeder Ebene. Jeder Test zeigt seinen Status, seine Laufzeit und, wo relevant, seinen Grund, sein Throwable und alle Probleme, die er ausgelöst hat, etwa als riskant markiert zu sein. Tests, die nicht bestanden haben, sind standardmäßig aufgeklappt; die bestandenen bleiben eingeklappt. Mit der Option --testdox bezeichnet der Report Klassen und Methoden mit ihren aufbereiteten TestDox-Namen statt mit ihren PHP-Namen, was einen Ergebnisbericht in etwas verwandelt, das du jemandem in die Hand geben kannst, der nicht beruflich PHP liest.

otr-report installieren

otr-report wird als PHP Archive (PHAR) ausgeliefert. Der empfohlene Weg, es als Werkzeug im Projekt zu verwalten, ist Phive:

$ phive install otr-report

$ ./tools/otr-report --version

Du kannst die PHAR auch direkt von phar.phpunit.de herunterladen. Wie bei PHPUnit selbst rate ich davon ab, es mit Composer zu installieren.

Es braucht nichts weiter als eine OTR-Logdatei, was bedeutet, dass es deinem Testlauf nichts hinzufügt außer einer einzigen Kommandozeilenoption. Die Daten waren die ganze Zeit da, bei jedem Lauf, und haben darauf gewartet, gelesen zu werden.

Jetzt kannst du sie lesen.