Testabdeckung pro Modul statt projektweit messen und durchsetzen
AI generated
@test
assert
PHPUnit · Coverage · CI/CD
Testabdeckung pro Modul statt projektweit
Warum eine globale Prozentzahl truegt

Eine einzelne projektweite Coverage-Zahl klingt beruhigend, kann aber genau die Kernmodule verstecken, die am dringendsten Tests braucht. Wer Coverage-Schwellenwerte pro Modul definiert und in der CI-Pipeline durchsetzt, schuetzt gezielt die Bereiche mit dem hoechsten Risiko, statt sich von einem guenstigen Durchschnitt taeuschen zu lassen.

14 Min. Lesezeit Code Coverage CI/CD Modularchitektur

1. Das Problem mit einer einzigen globalen Coverage-Zahl

Eine projektweite Coverage-Zahl von siebzig Prozent klingt nach einem soliden Testfundament. Sie sagt aber nichts darueber aus, wie diese siebzig Prozent zustande kommen. In einem typischen Projekt verteilt sich Testabdeckung selten gleichmaessig: Utility-Klassen mit trivialer Logik und viele generierte oder einfache Getter-Setter-Strukturen erreichen fast mühelos hundert Prozent, waehrend die eigentliche Geschaeftslogik, etwa Preisberechnung, Rabattregeln oder Checkout-Validierung, bei zwanzig Prozent verharrt, ohne dass die globale Zahl das sichtbar macht.

Dieses Verstecken-Effekt ist besonders gefaehrlich, weil Teams sich auf die globale Zahl als Erfolgsmetrik verlassen und in dem Glauben handeln, ausreichend abgesichert zu sein. Ein einzelner Bug in einem schlecht getesteten Kernmodul kann dann trotz insgesamt guter Coverage-Zahl in Produktion auftauchen, weil die Statistik den Mangel an genau der Stelle verschleiert hat, an der er am teuersten gewesen waere.

2. Eine konkrete Beispielrechnung zum Verstecken-Effekt

Nehmen wir ein Projekt mit drei Modulen: einem Utility-Modul mit 2000 Zeilen und 95 Prozent Abdeckung, einem Reporting-Modul mit 1500 Zeilen und 80 Prozent Abdeckung, sowie einem Checkout-Modul mit 500 Zeilen und nur 20 Prozent Abdeckung. Die projektweite, zeilengewichtete Coverage betraegt dann etwa 75 Prozent, ein auf den ersten Blick guter Wert. Das kleine, aber geschaeftskritische Checkout-Modul mit seiner duennen Abdeckung faellt dabei rechnerisch kaum ins Gewicht, obwohl genau hier ein Fehler den groessten finanziellen Schaden anrichten wuerde.

Wird stattdessen fuer jedes Modul ein eigener Schwellenwert definiert, etwa neunzig Prozent fuer Utility, siebzig Prozent fuer Reporting und funfundsiebzig Prozent fuer Checkout, faellt der Checkout-Wert von zwanzig Prozent sofort als klarer Verstoss auf und blockiert den Build, statt in einem guenstigen Durchschnitt zu verschwinden. Genau dieser Mechanismus macht modul-basierte Schwellenwerte einer einzelnen globalen Zahl ueberlegen.

3. Coverage pro Verzeichnis mit PHPUnit erzeugen

PHPUnit selbst erzeugt Coverage-Reports meist projektweit, etwa als Clover-XML oder HTML-Report. Um Werte pro Modul zu erhalten, ist es sinnvoll, PHPUnit pro Modulverzeichnis separat mit einer eigenen phpunit.xml oder einem eigenen testsuite-Eintrag laufen zu lassen, oder den erzeugten Clover-Report anschliessend nach Verzeichnis auszuwerten. Beide Ansaetze liefern granularere Daten als ein einziger Gesamtlauf, wobei die separate Ausfuehrung pro Modul zusaetzlich den Vorteil hat, dass Module unabhaengig voneinander parallel getestet werden koennen.

Ein Clover-XML-Report enthaelt fuer jede Datei die Anzahl abgedeckter und gesamter Statements, sodass sich daraus mit einem einfachen Skript eine Aggregation pro Verzeichnis berechnen laesst. Dieses Skript kann dann pro Modul pruefen, ob der jeweils definierte Schwellenwert erreicht wurde, und bei einer Unterschreitung den Build mit einem nicht-null Exit-Code beenden.


<?php
// tools/coverage-per-module.php
declare(strict_types=1);

$thresholds = [
    'app/code/Vendor/Checkout' => 75.0,
    'app/code/Vendor/Pricing' => 85.0,
    'app/code/Vendor/Reporting' => 60.0,
];

$clover = simplexml_load_file(__DIR__ . '/../var/coverage/clover.xml');
$exitCode = 0;

foreach ($thresholds as $modulePath => $minCoverage) {
    [$covered, $total] = [0, 0];

    foreach ($clover->xpath("//file[contains(@name, '{$modulePath}')]") as $file) {
        $metrics = $file->metrics;
        $covered += (int) $metrics['coveredstatements'];
        $total += (int) $metrics['statements'];
    }

    $actual = $total > 0 ? ($covered / $total) * 100 : 0.0;

    if ($actual < $minCoverage) {
        fwrite(STDERR, sprintf(
            "FEHLER: %s hat nur %.1f%% Coverage (mindestens %.1f%% erforderlich)\n",
            $modulePath, $actual, $minCoverage
        ));
        $exitCode = 1;
    } else {
        fwrite(STDOUT, sprintf("OK: %s hat %.1f%% Coverage\n", $modulePath, $actual));
    }
}

exit($exitCode);

4. Wie man sinnvolle Schwellenwerte pro Modul festlegt

Ein pauschaler Schwellenwert fuer alle Module, etwa 'ueberall achtzig Prozent', wiederholt im Kleinen den gleichen Fehler wie die globale Zahl, nur auf Modulebene verschoben. Sinnvoller ist es, den Schwellenwert an das Geschaeftsrisiko des Moduls zu koppeln: Zahlungsabwicklung, Preisberechnung und Bestandsverwaltung rechtfertigen hohe Werte von achtzig bis neunzig Prozent, waehrend ein internes Admin-UI-Modul mit ueberwiegend deklarativem Layout-Code auch mit vierzig oder fuenfzig Prozent ausreichend abgesichert sein kann.

Bei neuen Modulen empfiehlt sich, den Schwellenwert bewusst hoch anzusetzen, etwa achtzig Prozent von Anfang an, da hier keine Legacy-Last existiert, die eine niedrigere Zahl rechtfertigen wuerde. Bei bestehenden Modulen mit historisch gewachsener, duenner Abdeckung ist ein niedrigerer Startwert realistisch, der dann ueber ein Ratchet-Prinzip schrittweise angehoben wird, sobald neue Tests hinzukommen.

5. Durchsetzung in der CI-Pipeline

Damit modul-basierte Schwellenwerte wirken, muessen sie als eigener, blockierender Schritt in der CI-Pipeline verankert sein, nicht nur als informativer Report, der nach dem Merge betrachtet wird. Ein typisches Setup fuehrt PHPUnit mit aktivierter Coverage-Erfassung aus, erzeugt einen Clover-Report, und laesst anschliessend ein Skript wie im vorherigen Abschnitt ueber die definierten Schwellenwerte laufen. Schlaegt eine Modul-Pruefung fehl, bricht die Pipeline mit einem klaren, modul-spezifischen Fehlertext ab, statt nur eine generische 'Coverage zu niedrig'-Meldung auszugeben.

Wichtig ist, diese Pruefung fuer jeden Merge-Request oder Pull-Request laufen zu lassen, nicht nur periodisch auf dem Hauptzweig. Nur so wird verhindert, dass ein Entwickler versehentlich ein Modul unter den Schwellenwert druecken kann, ohne dass es sofort auffaellt. Ein spaeter periodischer Check faengt das Problem erst, nachdem der schlecht getestete Code bereits im Hauptzweig gelandet ist.


# .gitlab-ci.yml (Ausschnitt)
coverage-per-module:
  stage: test
  script:
    - vendor/bin/phpunit --coverage-clover var/coverage/clover.xml
    - php tools/coverage-per-module.php
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

6. Grenzen der modul-basierten Betrachtung

Modul-basierte Schwellenwerte loesen nicht automatisch das Problem oberflaechlicher Tests, die zwar Zeilen ausfuehren, aber keine sinnvollen Assertions treffen. Ein Modul kann formal neunzig Prozent Coverage erreichen und trotzdem grobe Fehler enthalten, wenn Tests lediglich Methoden aufrufen, ohne die Rueckgabewerte oder Seiteneffekte zu pruefen. Coverage misst ausgefuehrten Code, nicht verifiziertes Verhalten, und dieser Unterschied bleibt auch bei granularer Betrachtung pro Modul bestehen.

Ergaenzend lohnt sich deshalb der Einsatz von Mutation Testing fuer die kritischsten Module, das gezielt kleine Aenderungen am Produktionscode vornimmt und prueft, ob mindestens ein Test dadurch fehlschlaegt. Ein Modul mit hoher Zeilen-Coverage, aber niedriger Mutation Score, signalisiert, dass die vorhandenen Tests zwar Code ausfuehren, aber Fehler nicht zuverlaessig erkennen wuerden.

7. Wie Modulgrenzen fuer die Coverage-Messung definiert werden

Die Aussagekraft modul-basierter Schwellenwerte haengt direkt davon ab, wie sinnvoll die Modulgrenzen selbst gezogen sind. In einem Magento-Projekt bieten sich die einzelnen app/code/Vendor/Modulname-Verzeichnisse als natuerliche Grenze an, da sie ohnehin die Deployment- und Verantwortlichkeitseinheit bilden. Ein zu grobes Modul, das mehrere fachlich unabhaengige Bereiche buendelt, verwaesert die Aussagekraft des Schwellenwerts auf die gleiche Weise wie eine projektweite Zahl, nur auf kleinerer Flaeche.

Bei sehr grossen Modulen kann es sinnvoll sein, zusaetzliche Unterebenen zu definieren, etwa getrennte Schwellenwerte fuer Model-, Service- und Controller-Schichten innerhalb eines Moduls, wenn diese Schichten deutlich unterschiedliche Risikoprofile haben. Diese Feinsteuerung sollte aber bewusst sparsam eingesetzt werden, da zu viele einzelne Schwellenwerte die Konfiguration unuebersichtlich machen und die Pflegekosten steigen.

8. Transparente Reports fuer das Team schaffen

Neben der reinen Durchsetzung in der Pipeline lohnt sich ein regelmaessig aktualisiertes Dashboard, das die Coverage-Werte aller Module nebeneinander zeigt, idealerweise mit einer farblichen Kennzeichnung, welche Module ueber, nahe an, oder unter ihrem Schwellenwert liegen. Ein solches Dashboard macht sichtbar, welche Module tatsaechlich Aufmerksamkeit brauchen, und verhindert, dass die Diskussion ueber Testabdeckung auf eine einzelne, wenig aussagekraeftige Zahl reduziert wird.

Ergaenzend hilft ein kurzer Kommentar im Merge-Request, der automatisiert die Coverage-Aenderung pro betroffenem Modul auflistet, etwa 'Checkout-Modul: 42% auf 47%, Pricing-Modul: unveraendert bei 88%'. Diese unmittelbare, kontextbezogene Rueckmeldung wirkt staerker auf das Verhalten von Entwicklern als ein monatlicher Gesamtbericht, der oft erst mit deutlicher Verzoegerung wahrgenommen wird.

9. Line-Coverage versus Branch-Coverage pro Modul

Selbst ein modul-basierter Schwellenwert kann taeuschen, wenn er ausschliesslich auf Zeilen-Coverage basiert. Eine einzelne Zeile mit einer komplexen bedingten Bedingung, etwa if ($status === 'vip' && $amount > 100 || $isPromoActive), gilt bereits als abgedeckt, sobald sie einmal in irgendeiner Kombination durchlaufen wurde, auch wenn drei von vier moeglichen Bedingungspfaden nie getestet wurden. Fuer risikoreiche Module wie Checkout oder Preisberechnung ist Branch-Coverage daher eine deutlich aussagekraeftigere Messgroesse als reine Zeilen-Coverage.

PHPUnit unterstuetzt ueber die zugrunde liegende Xdebug- oder PCOV-Treiber-Integration auch Pfad- und Branch-Metriken, die sich in der phpunit.xml gezielt fuer die kritischsten Module aktivieren lassen, waehrend weniger riskante Module weiterhin nur mit einfacher Zeilen-Coverage geprueft werden, um die Ausfuehrungszeit der Coverage-Erfassung nicht unnoetig fuer das gesamte Projekt zu erhoehen.


<!-- phpunit.xml, Ausschnitt fuer ein Modul mit Branch-Coverage-Pflicht -->
<coverage>
    <report>
        <html outputDirectory="var/coverage/html"/>
        <clover outputFile="var/coverage/clover.xml"/>
    </report>
    <include>
        <directory suffix=".php">app/code/Vendor/Checkout/Model</directory>
    </include>
</coverage>
<!-- Ausfuehrung mit Pfad-Coverage fuer das Checkout-Modul -->
<!-- vendor/bin/phpunit --path-coverage --coverage-clover var/coverage/checkout-clover.xml -->
Modul-Typ Empfohlener Schwellenwert Begruendung
Zahlung / Checkout 80-90% Hohes finanzielles Risiko bei Fehlern
Preisberechnung / Rabatte 80-90% Direkter Umsatzeinfluss, komplexe Regeln
Reporting / Export 55-65% Fehler sichtbar, aber selten geschaeftskritisch
Admin-UI / Layout 35-50% Ueberwiegend deklarativ, geringes Logikrisiko
Neue Module ohne Legacy-Last 80%+ Kein historischer Nachholbedarf, hoher Standard von Anfang an

Mironsoft

Testautomatisierung, Magento-Qualitätssicherung und CI-Integration

Tests, die echte Fehler finden statt nur grün zu leuchten?

Wir prüfen bestehende PHPUnit-Suiten auf Implementierungsdetail-Tests, flaky Tests und fehlende Coverage an kritischen Stellen und bauen daraus eine Teststrategie, die bei jedem Magento-Update wirklich Sicherheit gibt.

Test-Audit

Bestehende Suiten auf Mocking-Antipatterns und blinde Flecken prüfen.

Teststrategie

Unit-, Integrations- und MFTF-Tests sinnvoll für Magento-Projekte kombinieren.

CI-Integration

Schnelle, zuverlässige Testläufe in GitLab CI oder GitHub Actions einrichten.

10. Zusammenfassung

Modul-Coverage: Das Wichtigste auf einen Blick

Problem

Eine globale Coverage-Zahl kann schlecht getestete Kernmodule statistisch verstecken.

Loesung

Eigene Schwellenwerte pro Modul, gekoppelt an das jeweilige Geschaeftsrisiko.

Umsetzung

Clover-Report nach Verzeichnis auswerten, Skript in der CI-Pipeline blockierend einsetzen.

Grenze

Coverage misst Ausfuehrung, nicht Verifikation, Mutation Testing ergaenzt die kritischsten Module.

11. FAQ: Modul-Coverage: Das Wichtigste auf einen Blick

1Warum reicht eine projektweite Coverage-Zahl nicht aus?
Weil sie zeilengewichtet ueber alle Dateien mittelt und dadurch gut getestete, aber unwichtige Utility-Klassen schlecht getestete, geschaeftskritische Module rechnerisch ausgleichen koennen. Ein Kernmodul mit zwanzig Prozent Coverage faellt in einer Gesamtzahl von siebzig Prozent kaum auf.
2Wie erzeuge ich Coverage-Daten pro Modul mit PHPUnit?
Entweder man laesst PHPUnit pro Modulverzeichnis separat mit eigener Konfiguration laufen, oder man erzeugt einen projektweiten Clover-XML-Report und aggregiert die enthaltenen Datei-Metriken anschliessend per Skript nach Verzeichnis.
3Wie hoch sollte der Schwellenwert fuer ein Checkout-Modul sein?
Ueblich sind achtzig bis neunzig Prozent, da Fehler in der Checkout-Logik direkten finanziellen Schaden verursachen koennen. Der genaue Wert sollte sich am tatsaechlichen Geschaeftsrisiko und nicht an einer pauschalen Regel orientieren.
4Muss jedes Modul den gleichen Schwellenwert haben?
Nein, im Gegenteil, ein einheitlicher Schwellenwert fuer alle Module wiederholt im Kleinen den Fehler der globalen Zahl. Sinnvoll ist eine Staffelung nach Geschaeftsrisiko, mit hohen Werten fuer Zahlungs- und Preislogik und niedrigeren Werten fuer rein deklarative Admin-UI-Bereiche.
5Wie setze ich modul-basierte Schwellenwerte in der CI-Pipeline durch?
Ueber einen eigenen Pipeline-Schritt, der nach dem PHPUnit-Lauf mit Coverage-Erfassung ein Skript ausfuehrt, das die definierten Schwellenwerte pro Modul prueft und bei Unterschreitung mit einem nicht-null Exit-Code abbricht, sodass der Merge-Request blockiert wird.
6Was passiert, wenn ein Modul unter dem Schwellenwert liegt?
Die CI-Pipeline sollte in diesem Fall fehlschlagen und eine klare, modul-spezifische Fehlermeldung ausgeben, damit der Entwickler sofort erkennt, welches Modul betroffen ist und wie weit die tatsaechliche Coverage vom geforderten Wert entfernt liegt.
7Loest eine hohe Modul-Coverage automatisch das Problem oberflaechlicher Tests?
Nein, Coverage misst nur, welcher Code ausgefuehrt wurde, nicht ob die Tests sinnvolle Assertions treffen. Ein Modul kann formal hohe Coverage haben und trotzdem schwache Tests, ergaenzend hilft hier Mutation Testing fuer die kritischsten Bereiche.
8Wie definiere ich sinnvolle Modulgrenzen fuer die Coverage-Messung?
In einem Magento-Projekt bieten sich die einzelnen app/code/Vendor/Modulname-Verzeichnisse an, da sie bereits die natuerliche Deployment- und Verantwortlichkeitseinheit bilden. Zu grobe Module verwaessern die Aussagekraft des Schwellenwerts.
9Wie geht man mit neuen Modulen ohne Legacy-Last um?
Bei neuen Modulen empfiehlt sich, den Schwellenwert von Anfang an hoch anzusetzen, etwa achtzig Prozent, da hier keine historisch gewachsene, duenne Abdeckung existiert, die einen niedrigeren Startwert rechtfertigen wuerde.
10Wie mache ich Coverage-Werte fuer das ganze Team transparent?
Ueber ein regelmaessig aktualisiertes Dashboard, das alle Module nebeneinander mit farblicher Kennzeichnung zeigt, ergaenzt um automatisierte Kommentare in Merge-Requests, die die Coverage-Aenderung pro betroffenem Modul auflisten.