PHP Code Coverage: Wie du sie misst, was die Zahlen bedeuten und was du damit machst
Die meisten PHP-Teams gehen mit Code Coverage auf eine von zwei Arten um. Sie ignorieren sie, oder sie jagen einer Prozentzahl hinterher, ohne zu fragen, was diese eigentlich misst. Keins von beidem bringt dich näher an Code, dem du vertrauen kannst.
Code Coverage zeigt dir, welche Zeilen deine Testsuite ausgeführt hat. Sie sagt dir nicht, ob deine Tests einen Bug abfangen würden. Dieser Unterschied ist wichtiger als jede einzelne Zahl auf einem Dashboard, und er ist der Grund, warum ein Projekt bei 85 Prozent Coverage stehen und trotzdem kaputten Code in Produktion ausliefern kann.
Was Coverage tatsächlich misst
PHPUnit, das Standard-Testframework für PHP, berichtet Coverage in drei Varianten, und jede beantwortet eine andere Frage.
Line Coverage fragt, ob eine bestimmte Zeile während des Testlaufs mindestens einmal ausgeführt wurde. Sie ist die am einfachsten zu berechnende Metrik und die, die die meisten Dashboards standardmäßig anzeigen. Sie ist auch das schwächste Signal: Eine Zeile kann ausgeführt werden, ohne dass eine Assertion prüft, ob sie das Richtige getan hat.
Branch Coverage fragt, ob jeder Pfad durch eine Bedingung ausgeführt wurde. Eine if-Anweisung hat zwei Zweige. Line Coverage ist erfüllt, wenn einer der beiden Zweige einmal läuft. Branch Coverage verlangt beide, was den Fall abfängt, in dem deine Tests nur den Happy Path durchlaufen und nie das else.
Path Coverage geht noch weiter und prüft jede Kombination von Zweigen innerhalb einer Funktion. Sie wird in PHP-Tooling selten getrackt, weil die Anzahl der Pfade mit jeder zusätzlichen Bedingung exponentiell wächst, aber sie ist die theoretische Obergrenze, der sich Line und Branch Coverage annähern.
Für die meisten PHP-Projekte ist Branch Coverage das ehrlichere Ziel. Line Coverage allein sagt dir, eine Validierungsfunktion sei "abgedeckt", selbst wenn du nie getestet hast, was passiert, wenn die Validierung fehlschlägt, was meistens der Teil ist, der es wert wäre, getestet zu werden.
Coverage-Messung einrichten
PHPUnit berechnet Coverage nicht selbst. Es braucht einen Coverage-Treiber, und PHP gibt dir zwei echte Optionen: Xdebug und PCOV.
Xdebug ist die vertrautere Wahl, weil die meisten PHP-Entwickler es ohnehin für Step-Debugging installiert haben. Es berechnet Line-, Branch- und Path-Coverage und ist in die übrigen Profiling-Tools von Xdebug integriert. Der Preis ist Geschwindigkeit. Der Coverage-Modus von Xdebug bringt erheblichen Overhead mit sich, weil er den Interpreter instrumentiert, und bei einer großen Testsuite kann das aus einem Zwei-Minuten-Lauf einen Fünfzehn-Minuten-Lauf machen.
PCOV wurde gezielt gebaut, um dieses Problem zu lösen. Es berechnet nur Line Coverage, hat keine Debugging-Features und läuft im Coverage-Modus eine Größenordnung schneller als Xdebug, weil es sich in die Opcode-Ausführung der Zend Engine einklinkt, statt einen vollständigen Debugger obendrauf zu legen. Wenn deine CI-Pipeline bei jedem Push Coverage berechnet und jeder Lauf ein Timeout produziert oder deine Build-Minuten auffrisst, ist PCOV fast immer der richtige Trade-off.
Eine praktische Aufteilung: PCOV in CI für die Coverage-Zahl nutzen, die deine Pipeline gated, und lokal zu Xdebug greifen, wenn du Branch-Details zu einer bestimmten Klasse brauchst, an der du gerade aktiv testest.
# .env oder CI-Konfiguration
php -d pcov.enabled=1 vendor/bin/phpunit --coverage-html coverage/
<!-- phpunit.xml -->
<coverage>
<report>
<html outputDirectory="coverage/html"/>
<clover outputFile="coverage/clover.xml"/>
</report>
</coverage>
Halte den Coverage-Schritt getrennt von deinem schnellen Feedback-Loop. Entwickler sollten die Suite ohne Coverage-Instrumentierung für schnelle Iteration laufen lassen können und den instrumentierten Lauf CI oder einer bewussten lokalen Prüfung vorbehalten.
Einen Coverage-Report lesen, ohne sich selbst zu täuschen
Eine Coverage-Prozentzahl allein sagt dir fast nichts. Siebzig Prozent Coverage könnten bedeuten, dass die ungetesteten dreißig Prozent toter Code sind, den niemand aufruft, oder sie könnten bedeuten, dass die ungetesteten dreißig Prozent deine Zahlungsabgleich-Logik sind. Die Zahl ist in beiden Fällen identisch.
Was tatsächlich zählt, ist, wo die Lücken liegen. Öffne den HTML-Report, den PHPUnit generiert, und schau dir an, welche Dateien und welche Methoden ganz unten stehen. Ein Coverage-Report ist eine Karte, keine Note, und die nützliche Arbeit passiert, wenn du diese Karte liest.
Einige Muster lohnen sich besonders zu prüfen:
Fehlerbehandlungspfade sind der am häufigsten übersprungene Code in PHP-Anwendungen, weil das Schreiben eines Tests, der etwas absichtlich zum Scheitern bringt, mehr Aufwand bedeutet als einer für den Erfolgsfall. Wenn deine catch-Blöcke als ungetestet angezeigt werden, verdient das mehr Aufmerksamkeit als ein fehlender Getter.
Geschäftskritische Berechnungen, alles, was Geld, Berechtigungen oder Daten betrifft, die in einen Compliance-Report einfließen, verdienen Coverage unabhängig davon, was die Gesamtprozentzahl sagt. Eine Gesamtzahl von 60 Prozent mit voller Coverage auf der Abrechnungslogik ist in besserer Verfassung als 90 Prozent mit ungetesteter Abrechnungslogik.
Framework-Klebecode, Controller, die nichts weiter tun, als einen Service aufzurufen und eine Response zurückzugeben, kann man meist teilweise ungetestet lassen. Das Risiko dort ist gering, und Coverage auf Boilerplate zu jagen ist verschwendete Zeit, die kein reales Risiko reduziert.
Eine nützliche Gewohnheit: Wenn sich eine Coverage-Zahl nach einem Pull Request ändert, schau dir an, was sich konkret verschoben hat, bevor du entscheidest, ob die Änderung gut oder schlecht ist. Ein Rückgang von 82 auf 79 Prozent, weil jemand ein gut getestetes Feature zusammen mit etwas ungetesteter Konfigurations-Verkabelung hinzugefügt hat, ist nicht dasselbe wie ein Rückgang, weil jemand Tests für neue Geschäftslogik übersprungen hat.
Wo Coverage dich belügt
Ein Test kann eine Zeile ausführen, ohne etwas Sinnvolles über sie zu behaupten. Das ist der blinde Fleck, den jedes Coverage-Tool per Design hat: Coverage misst Ausführung, nicht Verifikation.
public function testCalculateDiscount(): void
{
$result = $this->pricingService->calculateDiscount($order);
$this->assertNotNull($result);
}
Dieser Test gibt dir volle Line Coverage auf calculateDiscount(). Er würde auch nicht fehlschlagen, wenn die Methode den falschen Rabattbetrag zurückgäbe, den falschen Prozentsatz anwendete oder die Währung der Bestellung komplett ignorierte. Die Zeile lief. Die Zahl stieg. Der Bug wurde trotzdem ausgeliefert.
Hier verdient sich Mutation Testing seinen Platz in einer ernsthaften PHP-Testsuite. Tools wie infection/infection funktionieren, indem sie absichtlich kleine Bugs, sogenannte Mutanten, in deinen Code einschleusen: einen Vergleichsoperator umdrehen, einen Rückgabewert ändern, einen Methodenaufruf entfernen, und dann deine Testsuite gegen jede mutierte Version laufen lassen. Wenn deine Tests trotz des eingebauten Bugs weiterhin grün sind, ist dieser Mutant "entkommen", und das zeigt dir genau, wo deine Assertions zu schwach sind, um einen echten Fehler abzufangen.
composer require --dev infection/infection
vendor/bin/infection --min-msi=70 --min-covered-msi=80
Der Mutation Score Indicator von Infection gibt dir eine Prozentzahl der Mutanten, die deine Suite abgefangen hat. Anders als rohe Coverage ist diese Zahl deutlich schwerer zu manipulieren, weil man sie nur besteht, wenn Assertions tatsächlich Verhalten prüfen und nicht nur Code ausführen. Infection laufen zu lassen ist langsamer als ein einfacher Coverage-Lauf, da es deine Suite einmal pro Mutant ausführt, weshalb die meisten Teams es nach Zeitplan oder auf kritischen Modulen laufen lassen statt bei jedem Commit.
Was du mit den Zahlen machst
Ein Coverage-Ziel funktioniert nur als Untergrenze, nicht als Ziel an sich. Ein Minimum von 70 Prozent Coverage auf neuen Pull Requests zu verlangen, fängt den Fall ab, in dem ein Feature ohne einen einzigen Test ausgeliefert wird. Es garantiert nicht, dass die vorhandenen Tests etwas taugen, weshalb eine Coverage-Grenze kombiniert mit periodischem Mutation Testing auf deinen kritischen Pfaden ein vollständigeres Bild ergibt als jede der beiden Metriken allein.
Für Kundenprojekte setzt Wolf-Tech Coverage-Erwartungen pro Modul statt als eine pauschale Zahl über eine gesamte Codebasis. Zahlungsabwicklung, Authentifizierung und alles, was unter einem Unique Constraint in eine Datenbank schreibt, wird nach einem hohen Maßstab bewertet, inklusive Mutation Testing auf der riskantesten Logik. Präsentationsschichten und dünne Controller bekommen einen niedrigeren Maßstab, weil der Ertrag erschöpfender Tests dort gering ist.
Wenn du eine Codebasis ohne Coverage-Historie übernimmst, widersteh dem Drang, überall auf einmal Tests nachzuziehen. Beginne damit, den aktuellen Zustand als Baseline zu messen, und verlange dann über eine Diff-Coverage-Prüfung (die meisten CI-Coverage-Tools unterstützen das), dass neuer und geänderter Code dein Ziel erreicht, statt zu versuchen, die Zahl der gesamten Codebasis in einem Durchgang anzuheben. So bekommst du eine verbesserte Trendlinie, ohne den mehrwöchigen Umweg, Tests nachträglich in stabilen Code einzubauen, den niemand anfasst.
Coverage-Tooling ist günstig einzurichten und leicht misszuverstehen. Der Wert liegt darin, den Report als Ausgangspunkt für ein Gespräch über Risiko zu behandeln, nicht als Note, die es zu optimieren gilt. Ein Team, das seinen Coverage-Report liest und fragt, was in den wichtigen Teilen noch ungetestet ist, holt mehr aus PHPUnit heraus als ein Team, das einer Prozentzahl auf einem Dashboard hinterherjagt.
Wenn deine PHP-Codebasis Coverage-Zahlen hat, die auf dem Papier gut aussehen, du aber nicht sicher bist, ob die Tests eine echte Regression abfangen würden, ist genau diese Lücke zwischen Metrik und Realität das, wofür ein Code-Quality-Audit gemacht ist. Wolf-Tech prüft Testsuiten als Teil jeder Codebasis-Bewertung, nicht nur den Code, den sie testen. Melde dich unter hello@wolf-tech.io oder erfahre mehr darüber, wie wir arbeiten, auf wolf-tech.io.

