Schnellere Code Coverage

Schnellere Code Coverage

Murali Nandigama hat einmal geschrieben:

"Knowing what should be tested is beautiful, and knowing what is being tested is beautiful."

Test-Driven Development hilft bei ersterem und die Software-Metrik Code Coverage hilft bei letzterem.

Während es die Testsuite einer Software ausführt, kann PHPUnit verfolgen, welche Teile des Quellcodes dieser Software ausgeführt werden. Mit anderen Worten, wir können messen, inwieweit die Tests den Quellcode der Software abdecken.

Code Coverage fand seinen Weg in die PHP-Welt bereits 2003, als Derick Rethans begann, Xdebug um diese Funktion zu erweitern. Es sollte jedoch bis 2007 dauern, bis diese Funktion in eine stabile Version, Xdebug 2.0, aufgenommen wurde.

Die Code Coverage-Funktionalität von Xdebug war ursprünglich ziemlich langsam. Zum Beispiel dauerte es 2006 mehrere Stunden, um die Testsuite des eZ Components Projekts mit aktivierter Code Coverage auszuführen, verglichen mit wenigen Minuten ohne Code Coverage. Eine Verlangsamung der (Test-)Ausführungsgeschwindigkeit, wenn Code-Coverage-Informationen gesammelt werden, ist normal und nichts Spezifisches für PHP. Die massive Verlangsamung durch Xdebug war jedoch nicht erwartet worden. Und sie machte die Verwendung von Code Coverage mit PHPUnit im Entwicklungsalltag unpraktikabel.

Mit der Veröffentlichung von Xdebug 2.0.1 hat sich die Code Coverage-Performance dramatisch verbessert: Die Tests für das eZ Components Projekt mit Code Coverage auszuführen, dauerte jetzt nur noch ein paar Minuten länger als ohne Code Coverage.

Xdebug 2.6

Wenn Sie mit PHPUnit einen Code Coverage-Report erstellen wollen, dann müssen Sie eine eine Liste von Quellcodedateien konfigurieren, die Sie im Report sehen wollen. Schließlich sind Sie beispielsweise nicht an der Codeabdeckung der Bibliotheken von Drittanbietern interessiert, die Sie in Ihrem Projekt verwenden.

Standardmäßig sammelt Xdebug Abdeckungsinformationen für allen Code, der zwischen einem Aufruf von xdebug_start_code_coverage() und einem Aufruf von xdebug_stop_code_coverage() ausgeführt wird. PHPUnit ruft diese Funktionen jeweils am Anfang und am Ende der Ausführung eines Tests auf. Als Ergebnis sammelt Xdebug Code-Coverage-Informationen für den Code des Tests, den getesteten Code, den PHPUnit-eigenen Code, den von PHPUnit verwendeten Code von Drittanbietern und den Code von Drittanbietern, der von dem getesteten Code verwendet wird. PHPUnit filtert dann die gesammelten Codeabdeckungsinformationen basierend auf der konfigurierten Whitelist.

Natürlich ist das Sammeln und Verarbeiten von Codeabdeckungsinformationen, an denen Sie nicht interessiert sind, nur um sie dann wegzufiltern - was an sich schon wieder eine teure Operation ist - eine Verschwendung von Ressourcen. Aus diesem Grund hat Xdebug 2.6 Filterfunktionen für die Codeabdeckung und andere Funktionen wie z. B. die Funktionsverfolgung eingeführt. Mit der Funktion xdebug_set_filter() können Sie verhindern, dass Xdebug Code-Coverage-Informationen für Quellcodedateien sammelt und verarbeitet, an denen Sie nicht interessiert sind. Das spart Zeit und Speicher.

Zwischenlösung

Holger Woltersdorf war einer der ersten, der über ein Bootstrap-Skript für PHPUnit twitterte, das die Testausführung beschleunigen kann, indem es xdebug_set_filter() verwendet.

Tweet

Während dies bereits das große Potenzial der von Xdebug bereitgestellten nativen Filterung zeigte, konnte es sein volles Potenzial nicht ausschöpfen, da der Quellcode von PHPUnit selbst sowie der seiner Abhängigkeiten bereits geladen ist, wenn das Bootstrap-Skript der Testsuite eingebunden wird. Dies ist ein Problem, da Xdebug seine native Filterung nur für Quellcodedateien anwenden kann, die nach dem Aufruf von xdebug_set_filter() geladen wurden.

Während der FrOSCon 2018 im August haben wir eine Proof-of-Concept-Implementierung für die Erzeugung eines Bootstrap-Skripts entwickelt, das die native Filterung von Xdebug basierend auf der XML-Konfigurationsdatei von PHPUnit konfiguriert.

Tweet

Uns gefiel, wie der Proof-of-Concept funktionierte und wir beschlossen, es ein paar Wochen später beim PHPUnit Code Sprint in München zu einem richtigen Feature weiterzuentwickeln.

PHPUnit 7.4

Während des PHPUnit Code Sprint in München im September haben wir die Ideen aus den oben genannten Zwischenlösungen zu Funktionen weiterentwickelt, die PHPUnit nun out-of-the-box bietet.

Als erstes haben wir eine neue Kommandozeilenoption implementiert, --dump-xdebug-filter, die PHPUnit anweist, die Konfigurationsdatei phpunit.xml für ein Projekt zu laden und dann die konfigurierte Dateilist in ein PHP-Skript zu dumpen. Dieses Skript enthält den PHP-Code, der für die Konfiguration des Filter-Mechanismus von Xdebug erforderlich ist.

Dieses generierte Skript muss so früh wie möglich geladen werden, um einen Einfluss auf den Ressourcenverbrauch im Zusammenhang mit der Sammlung von Codeabdeckungsdaten zu haben. Das Laden dieses Skripts während des Bootstraps der Testsuite (konfiguriert entweder über die Kommandozeilenoption --bootstap oder die Konfigurationsdirektive bootstrap= in phpunit.xml) ist zu spät.

Wir haben dieses Problem durch die Implementierung einer weiteren neuen Kommandozeilenoption, --prepend, gelöst, die von PHPUnit so früh wie möglich interpretiert wird.

Real World-Benchmark

Hier ist ein Real World-Benchmark aus der Testsuite für die Codebasis von kartenmacherei.de:

Benchmark

Interessant ist, dass die Leistungssteigerung davon abhängt, wie PHPUnit installiert und verwendet wird:

  • Wenn PHPUnit mit Composer installiert wird, dauert die Ausführung der Tests und die Generierung eines Codeabdeckungsberichts 40 % weniger Zeit, wenn ein Xdebug-Filterskript mit "-prepend" geladen wird (75,96 Sekunden gegenüber 126,29 Sekunden).
  • Wenn PHPUnit aus einem PHP-Archiv (PHAR) verwendet wird, benötigt die Ausführung der Tests und die Generierung eines Code-Abdeckungsberichts 60 % weniger Zeit, wenn ein Xdebug-Filterskript mit "-prepend" geladen wird (33,72 Sekunden gegenüber 86,29 Sekunden).
  • Die Ausführung der Tests und die Generierung eines Code Coverage Reports benötigt 55% weniger Zeit, wenn PHPUnit aus einem PHP-Archiv (PHAR) verwendet wird, als wenn PHPUnit mit Composer installiert wird (33,72 Sekunden gegenüber 75,96 Sekunden)

Wir haben dies nicht weiter untersucht, gehen aber davon aus, dass die Unterschiede damit zusammenhängen, dass die phpunit.phar kein Klassen-Autoloading für die Quellcode-Dateien von PHPUnit selbst sowie dessen Abhängigkeiten verwendet.

Anwendungsbeispiel

Inzwischen fragen Sie sich sicher: Wie nutze ich diese neuen Funktionen, um die Code Coverage-Analyse für meine Testsuite zu beschleunigen? Keine Sorge, dazu kommen wir jetzt. Die Beispiele in diesem Abschnitt setzen voraus, dass Sie phpunit.phar nach tools/phpunit heruntergeladen haben. Ersetzen Sie in den Beispielen ./tools/phpunit durch ./vendor/bin/phpunit, falls Sie Composer zur Installation von PHPUnit verwenden.

Ihre Konfigurationsdatei phpunit.xml sollte ähnlich aussehen wie die unten gezeigte. Für den Zweck dieses Artikels sind wir nur am Abschnitt <filter> interessiert:

<?xml   version = " 1.0 "   encoding = " UTF-8 " ?>
< phpunit   bootstrap = " vendor/autoload.php "
          forceCoversAnnotation = " true "
          beStrictAboutCoversAnnotation = " true "
          beStrictAboutOutputDuringTests = " true "
          beStrictAboutTodoAnnotatedTests = " true "
          verbose = " true " >
     < testsuites >
         < testsuite   name = " default " >
             < directory   suffix = " Test.php " > tests </ directory >
         </ testsuite >
     </ testsuites >
     < filter >
         < whitelist   processUncoveredFilesFromWhitelist = " true " >
             < directory   suffix = " .php " > src </ directory >
         </ whitelist >
     </ filter >
</ phpunit >

Übrigens: Die oben gezeigte Datei phpunit.xml konfiguriert PHPUnit mit Best Practice-Vorgaben. Sie müssen sie nicht einmal kopieren und einfügen: Führen Sie einfach ./tools/phpunit --generate-configuration aus, um einen Assistenten zu starten, der Ihnen drei einfache Fragen stellt, bevor er eine solche Konfigurationsdatei für Ihr Projekt erzeugt.

Zurück zur Beschleunigung Ihrer Code Coverage-Analyse: Der erste Schritt ist die Generierung des Filter-Skripts für Xdebug mit der Option --dump-xdebug-filter:

$ ./tools/phpunit --dump-xdebug-filter build/xdebug-filter.php PHPUnit 7.5.1 by Sebastian Bergmann and contributors. Runtime: PHP 7.3.1 with Xdebug 2.7.0beta1 Configuration: /workspace/project/phpunit.xml Wrote Xdebug filter script to build/xdebug-filter.php

Das generierte Filterskript sieht wie folgt aus:

<?php  declare ( strict_types = 1 ) ;
if   ( ! \function_exists ( 'xdebug_set_filter' ) )   {
     return ;
}
 
\xdebug_set_filter (
     \XDEBUG_FILTER_CODE_COVERAGE ,
     \XDEBUG_PATH_WHITELIST ,
     [
         '/workspace/project/src'
     ]
) ;

Jetzt können wir die Option --prepend verwenden, um das Xdebug-Filterskript so früh wie möglich zu laden, wenn wir einen Code Coverage-Report erzeugen wollen:

$ ./tools/phpunit --prepend build/xdebug-filter.php \ --coverage-html build/coverage-report

Wie geht es weiter?

Die Geschwindigkeite der Sammlung und Verarbeitung von Code Coverage-Daten kann erheblich verbessert werden, wenn PHPUnit 7.4 (oder höher) und Xdebug 2.6 (oder höher) verwendet wird. Im Moment erfordert dies, dass PHPUnit mit --dump-xdebug-filter aufgerufen wird, um ein PHP-Skript mit Konfigurationscode für Xdebug jedes Mal zu erzeugen, wenn die Dateiliste in phpunit.xml geändert wird. PHPUnit muss dann mit --prepend aufgerufen werden, um dieses generierte PHP-Skript zu laden, bevor irgendein anderer Code geladen wird.

Wir denken darüber nach, wie wir es einfacher machen können, von den Leistungsverbesserungen bei der Sammlung und Verarbeitung von Codeabdeckungsdaten zu profitieren.

Ein Ansatz könnte sein, PHPUnit so zu verändern, dass es die konfigurierte Dateiliste automatisch in ein PHP-Skript mit dem Namen .phpunit.xdebug.filter speichert und diese Datei so früh wie möglich lädt, wenn sie existiert und Xdebug 2.6 (oder höher) verwendet wird. Dies würde es uns ermöglichen, die Befehlszeilenoptionen --dump-xdebug-filter und --prepend wieder loszuwerden.

Update (22. Juni 2021): Seit wir diesen Artikel geschrieben haben, sind die hier besprochenen Optionen --dump-xdebug-filter und --prepend bereits wieder aus PHPUnit verschwunden. Dank Verbesserungen in php-code-coverage wurden sie nicht mehr benötigt. Außerdem benutzen die meisten Entwickler heutzutage die PCOV-Erweiterung, um Code Coverage-Informationen auf Zeilenenbene zu sammeln und Xdebug 3 ist viel schneller als Xdebug 2.