@test
assert
PHPUnit · Code Coverage · Xdebug · PCOV · Magento 2
PHPUnit Coverage und Xdebug
für große Magento-Projekte nutzbar machen

Code Coverage klingt nach einem einfachen Konzept, wird aber in großen Magento 2 Projekten schnell zur Bremse: Xdebug verlangsamt die Test-Suite um Faktor 5 bis 10, Reports sind riesig, und ohne Konfiguration misst man das Falsche. PCOV, gezielte Whitelist-Konfiguration und CI-Schwellenwerte machen Coverage zu einem nützlichen Werkzeug statt einem Engpass.

15 Min. Lesezeit Xdebug · PCOV · Clover · PHPUnit 10/11 · Magento 2.4 PHP 8.2+ · CI/CD Integration

1. Warum Code Coverage in Magento-Projekten so schwierig ist

Magento 2 ist eine der komplexesten PHP-Anwendungen im E-Commerce-Umfeld. Tausende Klassen, tiefe Dependency-Injection-Hierarchien, generierte Proxy- und Factory-Klassen sowie ein modulares Architekturmodell machen es schwierig, Code Coverage sinnvoll zu messen. Wer naiv vendor/bin/phpunit --coverage-html coverage/ ausführt, wartet nicht selten zwanzig Minuten – und erhält danach einen Report, der vor allem zeigt, wie viel von Magentos eigenem Framework-Code durchlaufen wurde statt des eigenen Codes.

Das eigentliche Problem ist die fehlende Trennung zwischen gemessenem Code und ausgeführtem Code. PHPUnit misst standardmäßig alle Klassen, die während eines Tests geladen werden – einschließlich aller Magento-Core-Klassen, generierter Klassen und Vendor-Bibliotheken. Ein einziger Integrationstest kann dabei Hunderte von Klassen berühren, die niemand testen möchte. Ohne klare Konfiguration ist die Zahl bedeutungslos: 45 Prozent Coverage auf dem gesamten Projekt sagt wenig darüber aus, ob der eigene Business-Code ausreichend abgedeckt ist.

Hinzu kommt der Performance-Aspekt: Xdebug als Coverage-Driver ist zuverlässig, aber langsam. In einem Magento-Projekt mit mehreren hundert Unit-Tests kann Xdebug die Gesamtlaufzeit von dreißig Sekunden auf fünf Minuten erhöhen. In einer CI-Pipeline, die bei jedem Commit läuft, ist das ein erheblicher Engpass. Die Lösung liegt in einer Kombination aus dem richtigen Coverage-Driver, präziser Source-Filter-Konfiguration und einem durchdachten Schwellenwert-Konzept, das Coverage als kontinuierliche Metrik statt als einmaliges Ziel behandelt.

2. Xdebug vs. PCOV: der richtige Driver für den richtigen Einsatz

Für Code Coverage stehen in PHPUnit drei Driver zur Verfügung: Xdebug, PCOV und phpdbg. Xdebug ist der vollständigste, da er neben Coverage auch Debugging, Profiling und Remote-Debugging unterstützt. PCOV ist ein dedizierter Coverage-Driver ohne Debugging-Funktionalität, der erheblich weniger Overhead erzeugt. phpdbg ist in PHP eingebaut, aber seit PHPUnit 10 als deprecated markiert und sollte nicht mehr verwendet werden.

In der Praxis bedeutet das: Für lokale Entwicklung mit Debugging ist Xdebug die richtige Wahl – man kann Coverage aktivieren, wenn man sie braucht, und Debugging nutzen, wenn man Fehler untersucht. Für CI-Pipelines ohne Debugging-Bedarf ist PCOV die bessere Option: Es ist bis zu dreimal schneller als Xdebug und erzeugt identische Coverage-Daten. Wichtig ist, dass nie beide Extensions gleichzeitig aktiv sind – PCOV und Xdebug schließen sich gegenseitig aus, und PHPUnit gibt eine Warnung aus, wenn Xdebug aktiv ist aber Coverage nicht angefordert wird.


# php.ini für CI-Umgebung — nur PCOV, kein Xdebug
extension=pcov.so
pcov.enabled=1
pcov.directory=/var/www/html/app/code

# Xdebug für lokale Entwicklung (separate php.ini oder .env)
# xdebug.mode=coverage  (nur wenn Coverage benötigt wird)
# xdebug.mode=debug     (für normale Entwicklung)
# xdebug.mode=off       (für maximale Performance)

# PHPUnit-Aufruf mit explizitem Driver
vendor/bin/phpunit \
  --coverage-driver pcov \
  --coverage-clover build/coverage/clover.xml \
  --testsuite unit

# Xdebug-Modus per Umgebungsvariable steuern
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text

PCOV hat eine wichtige Einschränkung: Es misst nur Code, der innerhalb des konfigurierten pcov.directory liegt. Das ist gleichzeitig ein Vorteil: Man erzwingt damit, dass nur eigener Code gemessen wird. Wer pcov.directory=/var/www/html/app/code/Vendor/Module setzt, misst ausschließlich die eigenen Module und bekommt sofort aussagekräftigere Zahlen. In Docker-basierten Magento-Setups (wie dem Mark-Shust-Setup) installiert man PCOV als zusätzliche PHP-Extension und schaltet Xdebug in der CI-Konfiguration ab.

3. PHPUnit-Konfiguration für aussagekräftige Coverage

Die PHPUnit-Konfigurationsdatei phpunit.xml ist der zentrale Ort, um Coverage-Verhalten zu steuern. In PHPUnit 10 und 11 hat sich die Konfigurationsstruktur grundlegend geändert: Die alte <filter>-Konfiguration wurde durch <source> ersetzt, und Coverage-Prozentsätze werden jetzt im <coverage>-Block definiert. Wer mit einem älteren Magento-Projekt arbeitet, das noch PHPUnit 9 nutzt, muss die unterschiedlichen Konfigurationsformate kennen und je nach Projektstand das richtige verwenden.

Besonders wichtig ist die Konfiguration von pathCoverage. Branch-Coverage und Path-Coverage liefern detailliertere Informationen darüber, welche Verzweigungen im Code durchlaufen wurden – nicht nur welche Zeilen. Ein Code mit vielen If-Else-Konstrukten kann bei Zeilencoverage 100 Prozent anzeigen, obwohl nur ein Pfad durch jede Bedingung getestet wurde. Branch-Coverage offenbart diese Lücken. Der Performance-Overhead für Branch-Coverage mit PCOV ist minimal und rechtfertigt die deutlich aussagekräftigeren Daten.


<?xml version="1.0" encoding="UTF-8"?>
<!-- phpunit.xml — PHPUnit 11 configuration for Magento 2 module testing -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd"
         bootstrap="dev/tests/unit/framework/bootstrap.php"
         cacheDirectory=".phpunit.cache"
         colors="true"
         failOnWarning="true"
         failOnRisky="true">

  <testsuites>
    <testsuite name="unit">
      <directory>app/code/Mironsoft/*/Test/Unit</directory>
    </testsuite>
  </testsuites>

  <source restrictDeprecations="true">
    <include>
      <directory suffix=".php">app/code/Mironsoft</directory>
    </include>
    <exclude>
      <directory>app/code/Mironsoft/*/Test</directory>
      <directory>app/code/Mironsoft/*/Setup</directory>
    </exclude>
  </source>

  <coverage>
    <report>
      <clover outputFile="build/coverage/clover.xml"/>
      <html outputDirectory="build/coverage/html" lowUpperBound="50" highLowerBound="80"/>
    </report>
  </coverage>

</phpunit>

4. Source-Filter: nur relevanten Code messen

Der wichtigste Hebel für aussagekräftige Coverage-Zahlen ist die präzise Konfiguration des Source-Filters. In PHPUnit 11 definiert der <source>-Block, welche Dateien in die Coverage-Berechnung einbezogen werden. Die häufigste Fehlerquelle: Der Filter ist zu weit gefasst und schließt Klassen ein, die sinnvollerweise nicht getestet werden können oder sollen – Datenbankmigrationen, Setup-Skripte, DI-Konfigurationen und generierte Klassen.

Für Magento-Module gibt es klare Kategorien von Code, die aus der Coverage-Messung ausgeschlossen werden sollten: Setup/-Verzeichnisse mit InstallSchema- und UpgradeData-Klassen, da diese nicht durch Unit-Tests abgedeckt werden können. Test/-Verzeichnisse selbst, da Test-Klassen keine produktiven Klassen sind. Konfigurationsklassen wie Plugins, die ohne echten Magento-Kontext nicht testbar sind. Mit einem präzisen Ausschluss dieser Kategorien steigt die Coverage-Zahl auf eine Basis, die echten Fortschritt zeigt statt statistisches Rauschen zu erzeugen.

Ein weiterer wichtiger Aspekt ist die Verwendung des Attributs #[CoversClass] beziehungsweise der Annotation @covers in Test-Klassen. Damit wird die Coverage-Messung für einen Test auf eine spezifische Klasse beschränkt, statt alle während des Tests berührten Klassen zu zählen. Das verhindert, dass ein Test die Coverage einer Klasse erhöht, die er gar nicht direkt testen soll – ein häufiges Problem bei Tests, die viele Abhängigkeiten instanziieren.

5. Clover-Reports erzeugen und in CI auswerten

Das Clover-Format ist das standardisierte XML-Format für Coverage-Daten und wird von den meisten CI-Systemen und Code-Coverage-Plattformen wie Codecov, Coveralls und SonarQube verstanden. PHPUnit erzeugt Clover-Reports mit der Option --coverage-clover oder über die phpunit.xml-Konfiguration. Der Report enthält für jede Datei und jede Methode detaillierte Zeilenzähler: wie oft jede Zeile ausgeführt wurde und ob Bedingungen vollständig abgedeckt sind.

In einer GitLab CI Pipeline lässt sich der Clover-Report direkt als Coverage-Artefakt hochladen und für das Coverage-Badge im Repository nutzen. GitHub Actions unterstützt Codecov als Service, der Clover-Reports automatisch analysiert und als PR-Kommentar die geänderten Dateien mit ihrer Coverage anzeigt. Das Ziel dabei ist immer dasselbe: Nicht die absolute Coverage-Zahl des Projekts im Blick behalten, sondern sicherstellen, dass neu hinzukommender Code ausreichend getestet ist.


# .gitlab-ci.yml — Coverage-Integration mit PHPUnit und PCOV
test:unit:coverage:
  stage: test
  image: php:8.4-cli
  before_script:
    - pecl install pcov
    - docker-php-ext-enable pcov
    - composer install --no-interaction --prefer-dist
  script:
    - php -d pcov.enabled=1
          -d pcov.directory=app/code/Mironsoft
          vendor/bin/phpunit
          --coverage-clover build/coverage/clover.xml
          --coverage-text
          --colors=never
          --testsuite unit
  coverage: '/^\s*Lines:\s*\d+.\d+\%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: build/coverage/cobertura.xml
    paths:
      - build/coverage/
    expire_in: 7 days
  only:
    - merge_requests
    - main

6. Coverage-Schwellenwerte sinnvoll setzen

Coverage-Schwellenwerte sind ein zweischneidiges Schwert. Zu niedrige Schwellenwerte geben keine Sicherheit; zu hohe erzeugen Druck, Tests zu schreiben, die keinen echten Mehrwert liefern – sogenannte Coverage-getriebene Tests, die Zeilen ausführen statt Verhalten zu verifizieren. Die sinnvollste Strategie für Magento-Projekte ist ein gestaffeltes Schwellenwert-Konzept: unterschiedliche Mindest-Coverage für verschiedene Code-Kategorien.

Business-Logik in Service-Klassen und ViewModels sollte eine hohe Coverage von 80 bis 90 Prozent anstreben. Infrastrukturcode wie Repositories und Plugin-Klassen, die tief mit Magento-Internals integriert sind, kann auf 50 bis 60 Prozent abgesenkt werden, da vollständige Unit-Tests hier ohne Mocking-Aufwand kaum realisierbar sind. Konfigurationsklassen werden vollständig aus der Coverage-Messung ausgeschlossen. PHPUnit erlaubt es in neueren Versionen nicht mehr, minimale Coverage direkt in der Konfiguration zu erzwingen – stattdessen prüft man den Clover-Report im CI-Skript mit einem kleinen PHP-Script oder einem Tool wie infection/infection.

7. Coverage-Annotationen gezielt einsetzen

Die Annotation #[CoversClass(PriceCalculator::class)] in PHPUnit 10 und 11 ist mehr als eine Dokumentationshilfe. Sie schränkt die Coverage-Berechnung für diesen Test auf die angegebene Klasse ein. Das bedeutet: Auch wenn der Test intern weitere Klassen instantiiert, erhöht er nur die Coverage der deklarierten Klasse. Das verhindert, dass Tests indirekt die Coverage von Klassen erhöhen, die eigentlich durch eigene Tests abgedeckt werden sollten.

Die Annotation #[CoversNothing] ist das Gegenstück: Sie markiert Tests explizit als Coverage-irrelevant. Das ist nützlich für Smoke-Tests und Integrationstests, die primär die Gesamtfunktionalität prüfen sollen, nicht die Coverage einzelner Klassen erhöhen. Mit coversDefaultClass in der Test-Klassenebene lässt sich die Standardklasse für alle Test-Methoden in einer Testklasse festlegen, sodass nicht jede Methode einzeln annotiert werden muss.


<?php

declare(strict_types=1);

namespace Mironsoft\Pricing\Test\Unit\Model;

use Mironsoft\Pricing\Model\PriceCalculator;
use Mironsoft\Pricing\Model\TaxResolver;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

/**
 * Unit tests for PriceCalculator.
 * Coverage is restricted to PriceCalculator — TaxResolver is mocked.
 */
#[CoversClass(PriceCalculator::class)]
final class PriceCalculatorTest extends TestCase
{
    private PriceCalculator $subject;
    private TaxResolver&MockObject $taxResolver;

    protected function setUp(): void
    {
        $this->taxResolver = $this->createMock(TaxResolver::class);
        $this->subject     = new PriceCalculator($this->taxResolver);
    }

    #[Test]
    #[DataProvider('provideGrossPriceData')]
    public function calculatesGrossPriceCorrectly(
        float $net,
        float $taxRate,
        float $expectedGross
    ): void {
        $this->taxResolver
            ->method('getRateForProduct')
            ->willReturn($taxRate);

        $result = $this->subject->calculateGross(productId: 1, netPrice: $net);

        self::assertEqualsWithDelta($expectedGross, $result, 0.001);
    }

    public static function provideGrossPriceData(): array
    {
        return [
            'standard_vat'  => [100.00, 0.19, 119.00],
            'reduced_vat'   => [100.00, 0.07, 107.00],
            'zero_rated'    => [100.00, 0.00, 100.00],
        ];
    }
}

8. HTML-Report lokal für schnelles Feedback

Der HTML-Report von PHPUnit ist das leistungsstärkste Werkzeug für lokale Coverage-Analyse. Er zeigt für jede Datei exakt, welche Zeilen ausgeführt wurden (grün), welche nicht (rot) und welche vollständig durch Bedingungsabdeckung getestet sind. Besonders hilfreich ist die Drill-Down-Funktion: Vom Projekt-Überblick zur Datei, von der Datei zur einzelnen Methode, von der Methode zu jedem einzelnen Statement. Das macht es möglich, in wenigen Klicks die Bereiche mit der geringsten Coverage zu identifizieren.

Für maximale Performance beim lokalen Entwickeln empfiehlt sich ein zweistufiger Workflow: Zunächst läuft die Test-Suite ohne Coverage für schnelles Feedback (unter dreißig Sekunden), dann gezielt für einzelne Module mit Coverage, wenn man einen bestimmten Codebereich untersucht. PHPUnit unterstützt das mit dem --filter-Flag und dem Argument --coverage-filter, das die Coverage-Messung auf bestimmte Verzeichnisse beschränkt, ohne die Test-Suite zu ändern. Der erzeugte HTML-Report öffnet sich direkt im Browser und bleibt zwischen Sessions zwischengespeichert, solange die Quelldateien unverändert sind.

9. Coverage-Strategien im Vergleich

Es gibt mehrere konkurrierende Ansätze, wie man Coverage in großen PHP-Projekten handhabt. Die Wahl des Ansatzes beeinflusst direkt, wie aussagekräftig die Zahlen sind und wie viel Overhead die Test-Suite erzeugt.

Strategie Coverage-Driver Performance Empfehlung
Vollständige Projekt-Coverage Xdebug (coverage-Modus) Langsam (5–10×) Nur für finale Reports
Modul-spezifische Coverage PCOV mit pcov.directory Schnell (1,5–2×) Standard für CI-Pipelines
Mutation Testing Infection + PCOV Sehr langsam Nightly Build, nicht pro Commit
Diff Coverage (nur geänderter Code) PCOV + diff-filter Tool Sehr schnell Ideal für PR-Checks
Keine Coverage (nur Test-Ergebnis) Kein Driver Maximal schnell Lokaler Entwicklungsworkflow

Diff-Coverage ist eine besonders wertvolle Strategie für Projekte mit langer Historie und heterogener bestehender Coverage. Statt zu versuchen, die Coverage des gesamten Projekts zu erhöhen, wird geprüft, ob der im aktuellen Pull Request neu hinzugefügte oder geänderte Code ausreichend getestet ist. Tools wie diff-cover kombinieren den Git-Diff mit dem Clover-Report und zeigen nur Coverage-Lücken in geänderten Dateien. Das macht Coverage zu einem Qualitätsgatter für neuen Code, ohne dass Legacy-Code ohne Tests den Workflow blockiert.

Mironsoft

PHPUnit, Code Coverage und Test-Infrastruktur für Magento 2

Code Coverage, die echten Mehrwert liefert?

Wir richten PHPUnit und PCOV für euer Magento-Projekt ein, konfigurieren aussagekräftige Coverage-Reports und integrieren Schwellenwerte in eure CI/CD-Pipeline – ohne Overhead und ohne falsche Zahlen.

Coverage-Audit

Analyse der bestehenden Konfiguration, Identifikation falscher Schwellenwerte und Coverage-Lücken im Business-Code

PCOV-Setup

PCOV in Docker-Umgebung einrichten, Xdebug-Konflikte auflösen und CI-Pipeline für schnelle Coverage optimieren

Clover-Integration

Clover-Reports in GitLab CI oder GitHub Actions integrieren, Codecov-Anbindung und Coverage-Badge einrichten

10. Zusammenfassung

Code Coverage in großen Magento-Projekten ist nur dann ein nützliches Werkzeug, wenn sie richtig konfiguriert ist. Der wichtigste Schritt ist der Wechsel von Xdebug zu PCOV in CI-Umgebungen: Drei- bis fünfmal schnellere Coverage-Messung mit identischen Ergebnissen. Die präzise Source-Filter-Konfiguration über den <source>-Block in phpunit.xml stellt sicher, dass nur eigener Business-Code gemessen wird und nicht Magento-Core oder generierte Klassen. Clover-Reports in CI-Pipelines liefern die Datenbasis für Coverage-Badges, PR-Kommentare und historische Verläufe.

Die wichtigste Erkenntnis für die Praxis: Absolute Coverage-Zahlen auf Projektebene sind weniger wertvoll als differenzierte Schwellenwerte für verschiedene Code-Kategorien und Diff-Coverage für Pull Requests. Ein neues Modul mit 85 Prozent Coverage auf Business-Logik ist wertvoller als ein Projekt mit 70 Prozent Coverage auf allem, inklusive Datenbankmigrationen und Konfigurationsklassen. Coverage-Annotationen wie #[CoversClass] präzisieren die Messung weiter und verhindern, dass Tests unbeabsichtigt die Coverage von Klassen erhöhen, die eigene Tests verdienen.

PHPUnit Coverage für Magento — Das Wichtigste auf einen Blick

Driver-Wahl

PCOV für CI (3–5× schneller als Xdebug), Xdebug für lokales Debugging. Nie beide gleichzeitig aktiv lassen.

Source-Filter

Nur eigene Module messen. Setup-, Test- und Konfigurationsklassen explizit ausschließen. pcov.directory eng setzen.

Clover-Reports

Clover-XML in CI als Artefakt speichern. Codecov oder GitLab Coverage Report für PR-Feedback nutzen.

Schwellenwerte

Differenzierte Schwellenwerte: Business-Logik 80–90%, Infrastruktur 50–60%. Diff-Coverage für PR-Qualitätsgatter.

11. FAQ: PHPUnit Coverage und Xdebug für Magento-Projekte

1Warum ist Xdebug für Coverage so langsam?
Xdebug ist als vollständiger Debugger gebaut und erzeugt viel mehr Overhead als nötig, um Zeilenausführung zu zählen. PCOV ist speziell für Coverage und 3–5× schneller.
2PCOV und Xdebug gleichzeitig installiert?
Installiert ja, gleichzeitig aktiv nein. XDEBUG_MODE=off für PCOV-Läufe. PHPUnit warnt bei aktivem Xdebug ohne expliziten Coverage-Mode.
3Zeilen-Coverage vs. Branch-Coverage?
Zeilen-Coverage: welche Zeilen liefen. Branch-Coverage: wurden alle Bedingungspfade durchlaufen? 100% Zeilen-Coverage ohne Branch-Coverage kann trotzdem Lücken haben.
4Wozu dient #[CoversClass]?
Beschränkt Coverage auf die genannte Klasse. Ohne sie erhöht ein Test Coverage aller berührten Klassen — auch solcher mit eigenen Tests. Macht Coverage präziser.
5Was aus Coverage-Messung ausschließen?
Setup-Klassen, Test-Klassen selbst, generierte Klassen, DI-Konfigurationen. Diese verzerren Zahlen oder sind nicht sinnvoll unit-testbar.
6Was ist Diff-Coverage?
Misst nur Coverage des geänderten Codes im PR. Ideal für Legacy-Projekte: Neuer Code muss Mindest-Coverage erfüllen, ohne dass alter Code blockiert.
7pcov.directory für Magento richtig setzen?
Auf eigenes Modul-Verzeichnis begrenzen: pcov.directory=/var/www/html/app/code/VendorName. Verhindert, dass PCOV Magento-Core und Vendor-Packages instrumentiert.
8Clover-Reports in GitLab CI integrieren?
coverage-Regex in .gitlab-ci.yml und Report als Artefakt. GitLab zeigt Cobertura-Format direkt als Coverage-Diff in Merge Requests an.
9Coverage-Schwellenwerte in phpunit.xml erzwingen?
In PHPUnit 10/11 nicht mehr per Konfiguration. Clover-Report im CI-Skript mit php-code-coverage-check auswerten und Build bei Unterschreitung fehlschlagen lassen.
10Lohnt sich Mutation Testing für Magento?
Ja, als Ergänzung. Infection prüft ob Tests Verhalten validieren. Wegen langer Laufzeit als Nightly Build auf kritischen Modulen, nicht pro Commit.