Im Jahr 2003 Derick Rethans freigegeben Xdebug 1.2 . Zum ersten Mal im PHP- Ökosystem ist es möglich, Daten zur Codeabdeckung zu erfassen. Im Jahr 2004 veröffentlichte Sebastian Bergmann PHPUnit 2 , wo er es erstmals verwendete. Entwickler können jetzt die Leistung ihrer Testsuiten mithilfe von Abdeckungsberichten messen.
Seitdem wurde die Funktionalität auf eine generische, unabhängige Komponente zur Abdeckung des PHP-Codes verschoben . PHPDBG und PCOV sind als alternative Treiber erschienen . Grundsätzlich hat sich der Kernprozess für Entwickler in den letzten 16 Jahren nicht geändert.
Im August 2020 wurde mit der Veröffentlichung von PHP-Code-Coverage 9.0 und den zugehörigen Releases PHPUnit 9.3 und Behat-Code-Coverage 5.0 eine neue Methode zur Schätzung der Abdeckung verfügbar.
Heute werden wir überlegen
- Eine kurze Tour durch die Grundlagen
- Einschränkungen
- Alternative Metriken
- Filialabdeckung
- Wege abdecken
- Einschließlich neuer Metriken
- Welche Metrik soll verwendet werden?
- Gibt es Gründe, keine neuen Metriken aufzunehmen?
- Ergebnis
Eine kurze Tour durch die Grundlagen
Die meisten PHP- Entwickler sind mit der Idee des automatisierten Codetests vertraut. Die Idee der Codeabdeckung ist eng mit automatisierten Tests verbunden und besteht darin, den Prozentsatz des Codes zu messen, der ausgeführt oder, wie sie sagen, durch Tests „abgedeckt“ wird. Zum Beispiel, wenn Sie den folgenden Code haben:
<?php
class PercentCalculator
{
public function __construct(int $numerator, int $denominator)
{
$this->numerator = $numerator;
$this->denominator = $denominator;
}
public function calculatePercent(): float
{
return round($this->numerator / $this->denominator * 100, 1);
}
}
Dann können wir einen PHPUnit- Test wie folgt schreiben :
<?php
class PercentCalculatorTest extends PHPUnit\Framework\TestCase
{
public function testTwentyIntoForty(): void
{
$calculator = new PercentCalculator(20, 40);
self::assertEquals(50.0, $calculator->calculatePercent());
}
}
Nach dem Ausführen des Tests bestätigt PHPUnit , dass wir in diesem einfachen Beispiel eine 100% ige Abdeckung erreicht haben:
Einschränkungen
Im obigen Beispiel gab es jedoch einen kleinen potenziellen Fehler. Wenn $ Nenner ist 0 , dann erhalten wir eine Division durch Null Fehler. Lassen Sie uns das beheben und sehen, was passiert:
<?php
class PercentCalculator
{
public function __construct(int $numerator, int $denominator)
{
$this->numerator = $numerator;
$this->denominator = $denominator;
}
public function calculatePercent(): float
{
// ,
//
//
return $this->denominator ? round($this->numerator / $this->denominator * 100, 1) : 0.0;
}
}
Obwohl Zeile 12 jetzt die ternäre if / else-Anweisung verwendet (und wir haben noch nicht einmal einen Test geschrieben, um zu überprüfen, ob unsere Nullbehandlung korrekt ist), sagt uns der Bericht, dass wir immer noch eine 100% ige Codeabdeckung haben.
Wenn ein Teil der Linie vom Test abgedeckt wird, wird die gesamte Linie als abgedeckt markiert . Dies kann irreführend sein!
Durch einfaches Berechnen, ob eine Zeile ausgeführt wird oder nicht, können andere Codekonstrukte häufig dieselben Probleme haben, zum Beispiel:
if ($a || $b || $c) { // **
doSomething(); // 100%
}
public function pluralise(string $thing, int $count): string
{
$string = $count . ' ' . $thing;
if ($count > 1) { // $count >= 2, - 100%
$string .= 's'; // $count === 1,
} // ,
return $string;
}
Alternative Metriken
Ab Version 2.3 konnte Xdebug nicht nur bekannte zeilenweise Metriken, sondern auch alternative Metriken für die Zweig- und Pfadabdeckung erfassen. Deriks Blogbeitrag über diese Funktion endete mit der berüchtigten Aussage:
„Es bleibt abzuwarten, bis Sebastian (oder jemand anderes) Zeit hat, PHP_CodeCoverage zu aktualisieren , um die Zweig- und Pfadabdeckung anzuzeigen . Viel Spaß beim Hacken!
Derik Retans, Januar 2015 "
Nachdem ich 5 Jahre auf diesen mysteriösen "jemand anderen" gewartet hatte, beschloss ich, alles selbst umzusetzen. Vielen Dank an Sebastian Bergman für die Annahme meiner Pull-Anfrage .
Filialabdeckung
Mit Ausnahme des einfachsten Codes gibt es Stellen, an denen der Ausführungspfad in zwei oder mehr Pfade abweichen kann. Dies geschieht an jedem Entscheidungspunkt, z. B. an jedem Wenn / Sonst oder Während . Jede "Seite" dieser Divergenzpunkte ist ein separater Zweig. Wenn es keinen Entscheidungspunkt gibt, enthält der Ausführungsthread nur einen Zweig.
Beachten Sie, dass trotz der Verwendung der Baummetapher ein Zweig in diesem Zusammenhang nicht mit einem Versionskontrollzweig identisch ist. Verwechseln Sie die beiden nicht!
Wenn die Verzweigungs- und Pfadabdeckung aktiviert ist, wird ein HTML- Bericht mit PHP-Code-Abdeckung generiertEnthält zusätzlich zum üblichen Leitungsabdeckungsbericht Add-Ons zum Anzeigen der Zweig- und Pfadabdeckung. So sieht die Zweigabdeckung mit demselben Codebeispiel wie zuvor aus:
Wie Sie sehen, zeigt das Pivot-Feld oben auf der Seite sofort an, dass wir zwar eine vollständige zeilenweise Abdeckung haben, dies jedoch nicht für die Zweig- und Pfadabdeckung gilt ( Pfade werden im nächsten Abschnitt ausführlich erläutert.
Außerdem wird Zeile 12 gelb hervorgehoben, um anzuzeigen, dass die Abdeckung unvollständig ist (eine Zeile mit einer Abdeckung von 0% wird wie gewohnt rot angezeigt).
Schließlich kann der aufmerksamere Benutzer feststellen, dass im Gegensatz zur zeilenweisen Abdeckung mehr Linien farblich hervorgehoben werden. Dies liegt daran, dass Verzweigungen basierend auf dem Ausführungsfluss innerhalb des PHP-Interpreters berechnet werden. Der erste Zweig jeder Funktion beginnt, wenn diese Funktion eingegeben wird. Dies steht im Gegensatz zur stringbasierten Abdeckung, bei der nur der Funktionskörper ausführbare Zeichenfolgen enthält und die Funktionsdeklaration selbst als nicht ausführbar gilt.
Zweige finden
Solche Unterschiede zwischen dem, was der PHP- Interpreter als logisch getrennten Codezweig betrachtet, und dem mentalen Modell des Entwicklers können das Verständnis von Metriken erschweren. Wenn Sie mich beispielsweise fragen, wie viele Zweige sich in berechnePerzent () befinden , würde ich diese 2 beantworten (ein Sonderfall für 0 und ein allgemeiner Fall). Wenn man sich jedoch den obigen Bericht über die Abdeckung des PHP-Codes ansieht , enthält diese einzeilige Funktion tatsächlich ... 4 Zweige ?!
Um zu verstehen, was der PHP- Interpreter bedeutet, gibt es im Upstream einen zusätzlichen Bericht zur Berichterstattung. Es zeigt eine erweiterte Version der Anzeige jedes Zweigs, mit deren Hilfe das im Quellcode verborgene Element effizienter identifiziert werden kann. Es sieht aus wie das:
Die Beschriftung lautet: „Nachfolgend finden Sie die Quellzeilen , die jeden Codezweig darstellen , den Xdebug gefunden hat . Beachten Sie, dass ein Zweig nicht mit einem String identisch sein muss: Ein String kann mehrere Zweige enthalten und daher mehr als einmal vorkommen. Denken Sie auch daran, dass einige Zweige implizit sein können, z. B. eine if-Anweisung hat immer ein anderes im logischen Ablauf, auch wenn Sie es nicht geschrieben haben. "
All dies ist noch nicht ganz offensichtlich, aber Sie können bereits verstehen, welche Zweige sich tatsächlich in berechnePerzent () befinden :
- Zweig 1 beginnt bei der Funktionseingabe und enthält die Prüfung $ this-> denominator.
- Die Ausführung wird dann in Zweige 2 und 3 aufgeteilt, je nachdem, ob der Sonderfall behandelt wird oder nicht;
- In Zweig 4 werden die Zweige 2 und 3 zusammengeführt. Er besteht aus der Rückgabe und dem Verlassen der Funktion.
Das mentale Zuordnen von Zweigen zu einzelnen Teilen des Quellcodes ist eine neue Fähigkeit, die ein wenig Übung erfordert. Aber es ist definitiv einfacher, es mit leicht lesbarem und verständlichem Code zu machen. Wenn Ihr Code voll von intelligenten Einzeilern ist, die wie in unserem Beispiel mehrere Logikelemente kombinieren, erwarten Sie mehr Komplexität im Vergleich zu Code, bei dem alles in mehreren Zeilen strukturiert und geschrieben ist, die vollständig den Zweigen entsprechen. Die gleiche Logik, die in diesem Stil geschrieben wurde, würde folgendermaßen aussehen:
Kleeblatt
Wenn Sie den PHP-Code-Coverage- Bericht im Clover- Format exportieren , um ihn auf ein anderes System zu übertragen, werden die Daten bei aktivierter verzweigungsbasierter Coverage in die Schlüssel für bedingte und abgedeckte Bedingungen geschrieben . Zuvor (oder wenn die Zweigabdeckung nicht aktiviert war) waren die exportierten Werte immer Null.
Wege abdecken
Pfade sind mögliche Kombinationen von Zweigen. Das Beispiel berechnePerzent () hat zwei mögliche Pfade, wie oben gezeigt:
- Zweig 1, dann Zweig 2, dann Zweig 4;
- Zweig 1, dann Zweig 3 und dann Zweig 4.
Oft ist die Anzahl der Pfade jedoch größer als die Anzahl der Verzweigungen, z. B. in Code, der viele Bedingungen und Schleifen enthält. Das folgende Beispiel aus der PHP-Code-Abdeckung hat 23 Zweige, aber es gibt tatsächlich 65 verschiedene Pfade, auf denen die Funktion ausgeführt werden kann:
final class File extends AbstractNode
{
public function numberOfTestedMethods(): int
{
if ($this->numTestedMethods === null) {
$this->numTestedMethods = 0;
foreach ($this->classes as $class) {
foreach ($class['methods'] as $method) {
if ($method['executableLines'] > 0 &&
$method['coverage'] === 100) {
$this->numTestedMethods++;
}
}
}
foreach ($this->traits as $trait) {
foreach ($trait['methods'] as $method) {
if ($method['executableLines'] > 0 &&
$method['coverage'] === 100) {
$this->numTestedMethods++;
}
}
}
}
return $this->numTestedMethods;
}
}
Wenn Sie nicht alle 23 Zweige finden können, denken Sie daran, dass foreach einen leeren Iterator akzeptieren kann und wenn es immer einen unsichtbaren anderen gibt .
Ja, das bedeutet, dass 65 Tests für eine 100% ige Abdeckung erforderlich sind.
Der HTML- Bericht zur PHP-Code-Abdeckung enthält wie Zweige eine zusätzliche Ansicht für jeden Pfad. Es zeigt, welche mit dem Teig bedeckt sind und welche nicht.
CRAP
Das Aktivieren der Pfadabdeckung wirkt sich weiter auf die angezeigten Metriken aus, nämlich den CRAP- Score . Die auf crap4j.org veröffentlichte Definition verwendet die historisch nicht verfügbare prozentuale Pfadabdeckungsmetrik in PHP als Eingabe für die Berechnung . Während in PHP immer eine zeilenweise Abdeckung verwendet wurde. Bei kleinen Features mit guter Abdeckung bleibt der CRAP- Score wahrscheinlich gleich oder nimmt sogar ab. Bei Funktionen mit vielen Ausführungspfaden und schlechter Abdeckung steigt der Wert jedoch erheblich an.
Einschließlich neuer Metriken
Die Zweig- und Pfadabdeckung wird zusammen aktiviert oder deaktiviert, da beide einfach unterschiedliche Darstellungen derselben zugrunde liegenden Codeausführungsdaten sind.
PHPUnit
Für PHPUnit 9.3+ sind zusätzliche Metriken standardmäßig deaktiviert und können entweder über die Befehlszeile oder über die Konfigurationsdatei phpunit.xml aktiviert werden , jedoch nur, wenn sie unter Xdebug ausgeführt werden . Der Versuch, diese Funktion bei Verwendung von PCOV oder PHPDBG zu aktivieren, führt zu einer Warnung vor Konfigurationsinkompatibilität, und es wird keine Abdeckung erfasst.
- In der Konsole verwenden , um die --path-Abdeckung Option : Verkäufer / ist / phpunit - Pfad-Abdeckung .
- Setzen Sie in phpunit.xml das pathCoverage- Attribut des Coverage- Elements auf true .
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage pathCoverage="true" processUncoveredFiles="true" cacheDirectory="build/phpunit/cache">
<include>
<directory suffix=".php">src</directory>
</include>
<report>
<text outputFile="php://stdout"/>
<html outputDirectory="build/coverage"/>
</report>
</coverage>
</phpunit>
In PHPUnit 9.3 wurde das Format der Konfigurationsdatei stark geändert , sodass die obige Struktur wahrscheinlich anders aussieht als Sie es gewohnt sind.
Behat-Code-Abdeckung
Für behat-code-cover 5.0+ erfolgt die Einstellung in behat.yml , das Attribut heißt branchAndPathCoverage . Wenn Sie versuchen, es mit einem anderen Treiber als Xdebug zu aktivieren , wird eine Warnung ausgegeben, es wird jedoch weiterhin eine Abdeckung generiert. Dies soll die Verwendung derselben Konfigurationsdatei in verschiedenen Umgebungen vereinfachen. Wenn nicht explizit konfiguriert, wird die neue Abdeckung standardmäßig aktiviert, wenn sie unter Xdebug ausgeführt wird .
Welche Metrik soll verwendet werden?
Persönlich werde ich ( Doug Wright ) die neuen Metriken verwenden, wann immer dies möglich ist. Ich habe sie mit verschiedenen Codes getestet, um zu sehen, was "normal" ist. Bei meinen Projekten werde ich höchstwahrscheinlich einen hybriden Ansatz verwenden, den ich unten zeigen werde. Bei kommerziellen Projekten sollte die Entscheidung, auf neue Metriken umzusteigen, natürlich vom gesamten Team getroffen werden, und ich freue mich darauf, ihre Ergebnisse mit meinen eigenen vergleichen zu können.
Meiner Meinung
Eine 100% ige Abdeckung der Pfade ist zweifellos der heilige Gral, und wo es sinnvoll ist, sie anzuwenden, ist sie eine gute Metrik, nach der man streben kann, auch wenn man dies nicht tut. Wenn Sie Tests schreiben, sollten Sie immer noch über Dinge wie Randfälle nachdenken. Durch die pfadbasierte Abdeckung können Sie sicherstellen, dass alles in Ordnung ist.
Wenn eine Methode jedoch Dutzende, Hunderte oder sogar Tausende von Pfaden enthält (was für ziemlich komplexe Dinge eigentlich nicht ungewöhnlich ist), würde ich keine Zeit damit verschwenden, Hunderte von Tests zu schreiben. Es ist ratsam, um zehn anzuhalten. Testen ist kein Selbstzweck, sondern ein Instrument zur Risikominderung und eine Investition in die Zukunft. Tests auszahlen sollte, und die Zeit auf verbrachte so vielTests werden sich wahrscheinlich nicht auszahlen. In solchen Situationen ist es am besten, eine gute Zweigstellenabdeckung anzustreben, da dies zumindest sicherstellt, dass Sie darüber nachdenken, was an jedem Entscheidungspunkt passiert.
Bei einer großen Anzahl von Pfaden (sie sind jetzt mit ehrlichem CRAP gut definiert) bewerte ich, ob der betreffende Code nicht zu viel bewirkt, und gibt es eine vernünftige Möglichkeit, ihn in kleinere Funktionen aufzuteilen (die bereits detaillierter analysiert werden können)? Manchmal nicht, und das ist in Ordnung - wir müssen nicht alle Projektrisiken beseitigen. Sogar über sie Bescheid zu wissen ist wunderbar. Es ist auch wichtig, sich daran zu erinnern, dass Funktionsgrenzen und ihre isolierten Komponententests eine künstliche Trennung der Logik darstellen und nicht die wahre Komplexität Ihrer gesamten Software. Daher würde ich empfehlen, keine großen Funktionen nur wegen der entmutigenden Anzahl von Ausführungspfaden zu unterbrechen. Tun Sie dies nur, wenn die Trennung die kognitive Belastung verringert und die Code-Wahrnehmung unterstützt.
Gibt es Gründe, keine neuen Metriken aufzunehmen?
Ja, Leistung. Es ist kein Geheimnis, dass Xdebug- Code im Vergleich zur normalen PHP- Leistung unglaublich langsam ist . Und wenn Sie die Abdeckung von Zweigen und Pfaden aktivieren, wird alles durch das Hinzufügen von Overheads für alle zusätzlichen Ausführungsdaten, die er jetzt verfolgen muss, erschwert.
Die gute Nachricht ist, dass die Behebung dieser Probleme den Entwickler dazu inspiriert hat, allgemeine Leistungsverbesserungen innerhalb der PHP-Code-Abdeckung vorzunehmen , von denen jeder profitieren wird, der Xdebug verwendet . Die Leistung von Testsuiten ist sehr unterschiedlich, daher ist es schwer zu beurteilen, wie sich dies auf die einzelnen Testsuiten auswirkt. Das Sammeln von auf Zeichenfolgen basierender Abdeckung ist jedoch ohnehin schneller.
Es ist immer noch etwa 3-5 mal langsamer, eine Abdeckung von Zweigen und Pfaden zu erstellen. Dies muss berücksichtigt werden. Erwägen Sie die selektive Aktivierung einzelner Testdateien anstelle der gesamten Testsuite oder eines nächtlichen Builds mit "besserer Abdeckung", anstatt jeden Push auszuführen.
Xdebug 3 ist aufgrund der Modularisierungs- und Leistungsarbeiten erheblich schneller als aktuelle Versionen. Daher sollten diese Einschränkungen nur für Xdebug 2 gelten . Mit Version 3 ist es trotz des Overheads beim Sammeln zusätzlicher Daten möglich, eine verzweigungs- und pfadbasierte Abdeckung in kürzerer Zeit als heute zu generieren, um eine zeilenweise Abdeckung zu erhalten!
Von Sebastian Bergmann durchgeführte Tests, Grafik von Derick Rethans
Ergebnis
Bitte testen Sie die neuen Funktionen und schreiben Sie uns. Sind sie hilfreich? Besonders interessant sind Ideen für eine alternative Visualisierung (möglicherweise aus anderen Sprachen).
Nun, ich bin immer an Ihrer Meinung über die normale Codeabdeckung interessiert.
Auf der PHP Russia am 29. November werden wir alle wichtigen Fragen zur PHP-Entwicklung diskutieren, was nicht in der Dokumentation enthalten ist, aber was Ihrem Code eine neue Ebene verleiht.
Nehmen Sie an der Konferenz teil: Hören Sie sich nicht nur Berichte an und stellen Sie Fragen an die besten Sprecher des PHP-Universums, sondern auch für die professionelle Kommunikation (endlich offline!) In einer warmen Atmosphäre. Unsere Communities: Telegramm , Facebook , VKontakte , YouTube .