von --log-junit bis zum HTML-Coverage-Report
Wer PHPUnit-Ergebnisse nur als Terminalausgabe betrachtet, verschenkt den größten Teil des Mehrwerts automatisierter Tests. JUnit XML, HTML-Coverage, Teamcity-Output und strukturierte Testdauer-Reports machen Test-Ergebnisse für CI-Systeme, Teams und Monitoring auswertbar – über einen einzelnen Build-Lauf hinaus.
Inhaltsverzeichnis
- 1. Warum Testberichte mehr sind als Terminal-Output
- 2. JUnit XML: Format, Konfiguration und Fallstricke
- 3. Coverage-Reports: HTML, Clover und Cobertura
- 4. Integration in GitHub Actions
- 5. Integration in GitLab CI und Jenkins
- 6. Testdauer-Reports und langsame Tests identifizieren
- 7. Artefakte und Retention richtig konfigurieren
- 8. Typische Fehler bei der CI-Ausgabe
- 9. Output-Formate im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Testberichte mehr sind als Terminal-Output
Die Ausgabe von PHPUnit im Terminal ist für den Entwickler beim lokalen Ausführen nützlich – sie ist grün, rot oder gelb, zeigt fehlschlagende Tests mit Stack-Trace und gibt am Ende eine Zusammenfassung. Für eine CI-Pipeline, ein Dashboard oder einen Team-Report ist die Terminalausgabe hingegen praktisch wertlos. Sie ist nicht maschinenlesbar, nicht persistierbar und nicht vergleichbar über Build-Läufe hinweg.
Der strukturierte Testbericht löst dieses Problem. JUnit XML ist das De-facto-Standardformat für Testergebnisse in CI-Systemen, weil es von GitHub Actions, GitLab CI, Jenkins, CircleCI, Azure DevOps und nahezu allen anderen CI-Plattformen nativ gelesen und als Trendgraph visualisiert werden kann. Ein HTML-Coverage-Report gibt Entwicklern eine klickbare Übersicht, welche Codezeilen durch Tests abgedeckt werden. Ein Testdauer-Report zeigt, welche Tests die Build-Zeit dominieren. Zusammen machen diese Berichte automatisierte Tests zu einem dauerhaften Asset für die Qualitätssicherung im Team – nicht nur zu einem Gating-Mechanismus im Deployment.
Dieser Beitrag zeigt, wie PHPUnit-Berichte für verschiedene CI-Systeme korrekt konfiguriert werden, welche Fallstricke bei JUnit XML in PHP-Projekten auftreten und wie Coverage-Reports mit minimalem Overhead in Pipelines integriert werden.
2. JUnit XML: Format, Konfiguration und Fallstricke
Das JUnit XML Format ist de facto ein inoffizieller Standard: Es gibt keine offizielle Spezifikation, aber nahezu alle CI-Systeme erwarten dasselbe Grundschema mit <testsuites>, <testsuite> und <testcase>-Elementen. PHPUnit erzeugt valides JUnit XML über den --log-junit-Parameter oder über die phpunit.xml-Konfiguration. Die Konfiguration in phpunit.xml ist vorzuziehen, weil sie versioniert, reproduzierbar und unabhängig vom CI-Skript ist.
Ein häufiger Fallstrick: PHPUnit erzeugt JUnit XML mit UTF-8-Kodierung, aber einige CI-Parser tolerieren keine XML-Sonderzeichen in Testnamen. Testklassenamen mit Doppelpunkten, Winkelklammern oder Sonderzeichen in Datenprovider-Argumenten führen dazu, dass der Parser die Datei nicht einlesen kann oder falsche Testnamen anzeigt. Die Lösung besteht darin, Testnamen in Datenprovidern kurz und alphanumerisch zu halten oder explizit zu benennen. PHPUnit 10+ sanitisiert Testnamen vor dem Schreiben der XML-Datei stärker als frühere Versionen, was die Kompatibilität verbessert hat.
<?xml version="1.0" encoding="UTF-8"?>
<!-- phpunit.xml — PHPUnit configuration with JUnit XML and Coverage -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
executionOrder="depends,defects"
requireCoverageMetadata="false"
beStrictAboutCoverageMetadata="false">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
</testsuites>
<!-- JUnit XML for CI test result reporting -->
<logging>
<junit outputFile="reports/junit.xml"/>
<testdox-text outputFile="reports/testdox.txt"/>
</logging>
<!-- Coverage report — requires Xdebug or PCOV -->
<coverage>
<report>
<html outputDirectory="reports/coverage/html" lowUpperBound="50" highLowerBound="90"/>
<clover outputFile="reports/coverage/clover.xml"/>
<cobertura outputFile="reports/coverage/cobertura.xml"/>
<text outputFile="reports/coverage/coverage.txt" showOnlySummary="true"/>
</report>
<include>
<directory suffix=".php">src</directory>
</include>
<exclude>
<directory>src/DataFixtures</directory>
<file>src/Kernel.php</file>
</exclude>
</coverage>
</phpunit>
Die Konfiguration erzeugt nach jedem Test-Lauf vier verschiedene Reports im Verzeichnis reports/. Dieses Verzeichnis wird in der Regel nicht in das Repository eingecheckt, sondern als CI-Artefakt hochgeladen. Wichtig: Das Verzeichnis muss vor dem PHPUnit-Aufruf existieren, sonst schlägt PHPUnit beim Schreiben der Datei mit einem Fatal Error fehl. Ein mkdir -p reports/coverage im CI-Skript vor dem PHPUnit-Aufruf verhindert diesen Fehler.
3. Coverage-Reports: HTML, Clover und Cobertura
PHPUnit unterstützt mehrere Coverage-Ausgabeformate, die für unterschiedliche Zwecke optimiert sind. HTML ist das einzige Format, das direkt für Menschen lesbar ist: Es erzeugt eine Navigation durch alle Klassen und Methoden mit farbiger Hervorhebung abgedeckter und nicht abgedeckter Zeilen. HTML-Coverage eignet sich für Entwickler, die lokal prüfen wollen, welche Zeilen ihrer neuen Funktion noch nicht durch Tests abgedeckt sind.
Clover XML ist das bevorzugte Maschinenformat für Code-Coverage-Dienste wie Coveralls, Codecov und SonarQube. Es enthält Zähler für Zeilen, Methoden, Klassen und Branches. Cobertura XML ist das Format, das von GitLab CI und Azure DevOps für die eingebaute Coverage-Visualisierung verwendet wird. Text-Coverage ist die kompakteste Zusammenfassung und eignet sich für den direkten Ausdruck in CI-Logs. Die Wahl des Formats hängt davon ab, welche Tools im Stack vorhanden sind – in der Regel werden zwei oder drei Formate parallel erzeugt, um verschiedene Empfänger zu bedienen.
Coverage-Reports sind rechenintensiv: Xdebug im Coverage-Modus kann die Test-Laufzeit um das Fünf- bis Zehnfache verlängern. Die Lösung besteht darin, Coverage-Reports nur in einem separaten CI-Job zu erzeugen, der nicht den kritischen Build-Pfad blockiert. Unit-Tests ohne Coverage laufen auf jedem Commit schnell durch; der Coverage-Job läuft nur auf dem Hauptbranch oder über Nacht.
4. Integration in GitHub Actions
GitHub Actions hat in den letzten Jahren native Unterstützung für Test-Summaries über den $GITHUB_STEP_SUMMARY-Mechanismus eingeführt. Daneben existieren Marketplace-Actions, die JUnit XML lesen und als Pull-Request-Kommentar oder Action-Summary ausgeben. Die einfachste und wartungsärmste Lösung ist die Kombination aus direktem --log-junit und dem offiziellen actions/upload-artifact für die Persistierung.
# .github/workflows/tests.yml
name: PHPUnit Tests
on:
push:
branches: [main, develop]
pull_request:
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: mbstring, xml, ctype, dom
coverage: none # no coverage = faster
- name: Install dependencies
run: composer install --no-interaction --prefer-dist
- name: Create reports directory
run: mkdir -p reports/coverage
- name: Run Unit Tests
run: vendor/bin/phpunit --testsuite Unit --log-junit reports/junit.xml
- name: Upload test results
uses: actions/upload-artifact@v4
if: always() # upload even on failure
with:
name: phpunit-results
path: reports/junit.xml
retention-days: 30
coverage:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Setup PHP with Xdebug
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: mbstring, xml, xdebug
coverage: xdebug
- name: Install dependencies
run: composer install --no-interaction --prefer-dist
- name: Run tests with coverage
run: |
mkdir -p reports/coverage
XDEBUG_MODE=coverage vendor/bin/phpunit \
--coverage-clover reports/coverage/clover.xml \
--coverage-html reports/coverage/html
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: reports/coverage/
retention-days: 14
Das Schlüsseldetail im Workflow ist if: always() beim Upload der JUnit-Datei. Ohne diese Bedingung wird das Artefakt nicht hochgeladen, wenn PHPUnit mit einem Fehlercode beendet – also genau dann, wenn die Ergebnisse am nützlichsten wären. GitHub Actions behandelt einen Upload-Step ohne if: always() als übersprungen, wenn ein vorheriger Step fehlschlug. Der JUnit-Bericht muss aber auch (und gerade) bei fehlschlagenden Tests hochgeladen werden, damit CI-Dashboards die Fehlermuster analysieren können.
5. Integration in GitLab CI und Jenkins
GitLab CI hat native Unterstützung für JUnit XML über das reports: junit-Schlüsselwort in der Artifact-Konfiguration. GitLab liest die Datei automatisch und zeigt die Testergebnisse im Merge-Request-Interface an, inklusive eines Vergleichs mit dem Ziel-Branch. Für Cobertura-Coverage zeigt GitLab inline im Diff, welche Zeilen durch Tests abgedeckt sind – ein sehr effektives Tool für Code-Reviews.
# .gitlab-ci.yml
stages:
- test
- coverage
unit-tests:
stage: test
image: php:8.4-cli
before_script:
- apt-get update -qq && apt-get install -y -qq git unzip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install --no-interaction --prefer-dist
- mkdir -p reports/coverage
script:
- vendor/bin/phpunit --testsuite Unit --log-junit reports/junit.xml
artifacts:
when: always # crucial: upload even on failure
reports:
junit: reports/junit.xml
paths:
- reports/junit.xml
expire_in: 30 days
coverage-report:
stage: coverage
image: php:8.4-cli
before_script:
- pecl install xdebug
- docker-php-ext-enable xdebug
- composer install --no-interaction --prefer-dist
- mkdir -p reports/coverage
script:
- XDEBUG_MODE=coverage vendor/bin/phpunit
--coverage-cobertura reports/coverage/cobertura.xml
--coverage-html reports/coverage/html
--coverage-text
coverage: '/^\s*Lines:\s*\d+.\d+\%/' # GitLab extracts this for badge
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: reports/coverage/cobertura.xml
paths:
- reports/coverage/
expire_in: 7 days
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Der reguläre Ausdruck nach coverage: extrahiert den Coverage-Prozentsatz aus der Textausgabe von PHPUnit und zeigt ihn als Badge im GitLab-Repository-Interface. Jenkins verwendet das JUnit Plugin, um JUnit XML zu verarbeiten: Nach dem PHPUnit-Step wird junit 'reports/junit.xml' in der Jenkinsfile-Pipeline aufgerufen. Jenkins zeichnet dann automatisch den Test-Trend über alle Builds hinweg auf.
6. Testdauer-Reports und langsame Tests identifizieren
Lange Test-Laufzeiten sind in PHP-Projekten ein häufiges Problem, das ohne Messung unsichtbar bleibt. PHPUnit bietet den Parameter --log-events-verbose-text und --report-useless-tests, um Hinweise auf problematische Tests zu geben. Strukturierter lassen sich Testdauern über JUnit XML auswerten: Das Format enthält in jedem <testcase>-Element ein time-Attribut in Sekunden, das sich mit einfachen Shell-Werkzeugen oder Python auswerten lässt.
Das --order-by=duration-Flag (PHPUnit 10+) führt die langsamsten Tests zuerst aus und macht in der Terminalausgabe sofort sichtbar, welche Tests die Gesamtlaufzeit dominieren. Ein Test, der drei Sekunden dauert, ist in einer Unit-Test-Suite fast immer ein Anzeichen für ein Architekturproblem: echte Datenbankverbindungen, HTTP-Requests ohne Mocking oder schlecht gemockte Services. Die Lösung liegt nicht in der Test-Konfiguration, sondern im Testdesign selbst.
7. Artefakte und Retention richtig konfigurieren
Die Retention-Zeit für Testartefakte ist eine Abwägung zwischen Speicherkosten und Analysetiefe. JUnit XML-Dateien sind klein (wenige Kilobyte bis wenige Megabyte) und können problemlos 90 Tage aufbewahrt werden, um Trendanalysen über mehrere Monate zu ermöglichen. HTML-Coverage-Reports sind deutlich größer (10–100 MB bei größeren Projekten) und sollten kürzer aufbewahrt werden – typischerweise 7–14 Tage.
Für langfristige Trend-Analysen empfiehlt sich das Exportieren der Kerndaten aus dem JUnit XML in eine Zeitreihendatenbank. Einfache Shell-Skripte können aus JUnit XML die Anzahl der Tests, die Fehlerquote und die Gesamtdauer extrahieren und in InfluxDB oder Prometheus schreiben. Grafana-Dashboards visualisieren dann die Entwicklung dieser Metriken über Wochen und Monate – ein deutlich mächtigeres Werkzeug als das eingebaute Trend-Chart der meisten CI-Systeme.
| CI-System | JUnit XML | Coverage-Format | Konfiguration |
|---|---|---|---|
| GitHub Actions | upload-artifact + Marketplace-Actions | Clover via Codecov Action | if: always() beim Upload |
| GitLab CI | Nativ via reports: junit |
Cobertura nativ | when: always in artifacts |
| Jenkins | JUnit Plugin (junit Step) |
Cobertura Plugin | Jenkinsfile post-always Block |
| CircleCI | store_test_results |
Clover via externen Dienst | path in test_results_path |
| Azure DevOps | PublishTestResults Task | Cobertura nativ | testResultsFiles: '**/junit.xml' |
8. Typische Fehler bei der CI-Ausgabe
Der häufigste Fehler: Das reports/-Verzeichnis existiert nicht, wenn PHPUnit versucht, die JUnit-Datei zu schreiben. PHPUnit erstellt fehlende Verzeichnisse nicht automatisch und bricht stattdessen mit einem Fatal Error ab. Ein mkdir -p reports/coverage vor dem PHPUnit-Aufruf im CI-Skript ist Pflicht. Der zweite häufige Fehler: Coverage-Reports werden in jedem Build-Job aktiviert, was die gesamte Pipeline stark verlangsamt. Coverage sollte auf einen separaten, optionalen Job beschränkt sein.
Ein dritter Fehler betrifft die XDEBUG_MODE-Umgebungsvariable. Ohne XDEBUG_MODE=coverage erzeugt Xdebug 3.x keine Coverage-Daten, selbst wenn Xdebug installiert und geladen ist. PHPUnit gibt in diesem Fall eine Warnung aus, aber die Coverage-Dateien bleiben leer. Der vierte Fehler: JUnit XML wird im CI-System nicht gefunden, weil der Pfad relativ zum Working Directory falsch ist. Absolute Pfade oder ein explizites cd vor dem PHPUnit-Aufruf verhindern dieses Problem.
9. Output-Formate im Vergleich
PHPUnit bietet verschiedene Ausgabeformate, die für unterschiedliche Empfänger optimiert sind. Die Wahl des richtigen Formats für den jeweiligen Kontext reduziert Komplexität und verbessert die Auswertbarkeit der Testergebnisse im Team.
10. Zusammenfassung
Strukturierte PHPUnit-Testberichte verwandeln automatisierte Tests von einem Gating-Mechanismus in ein dauerhaftes Qualitätsinstrument. JUnit XML ist das universelle Format für CI-Systeme und wird von GitHub Actions, GitLab CI, Jenkins und allen anderen relevanten Plattformen nativ unterstützt. Die Konfiguration gehört in phpunit.xml, nicht in CI-Skripte – so bleibt sie versioniert, reproduzierbar und unabhängig vom CI-System.
Coverage-Reports verlangsamen Tests erheblich und sollten in separate, nicht-blockierende Jobs ausgelagert werden. Das if: always()- bzw. when: always-Pattern beim Artefakt-Upload ist bei Testergebnissen Pflicht – die Datei wird genau dann gebraucht, wenn Tests fehlschlagen. Langfristige Trend-Analysen entstehen durch das Exportieren von Kerndaten aus JUnit XML in Zeitreihendatenbanken und deren Visualisierung in Grafana-Dashboards.
PHPUnit Testberichte und JUnit XML — Das Wichtigste auf einen Blick
JUnit XML Konfiguration
In phpunit.xml unter <logging> konfigurieren. mkdir -p reports/ vor PHPUnit-Aufruf ausführen. Immer if: always() beim CI-Artefakt-Upload setzen.
Coverage in separaten Job
Coverage-Reports kosten Laufzeit. Separater Job nur auf Hauptbranch. XDEBUG_MODE=coverage nicht vergessen. Clover für externe Dienste, Cobertura für GitLab/Azure.
CI-System-Spezifika
GitLab: reports: junit nativ. GitHub: upload-artifact + if: always(). Jenkins: JUnit Plugin. Alle: Artefakt-Pfade testen bevor in Produktion.
Testdauer analysieren
--order-by=duration zeigt langsame Tests zuerst. Zeit-Attribut in JUnit XML maschinell auswerten. Langsame Unit-Tests deuten auf Architekturprobleme hin.
11. FAQ: PHPUnit Testberichte und JUnit XML in CI
1Warum schlägt PHPUnit beim Schreiben der JUnit XML Datei fehl?
mkdir -p reports/coverage vor dem PHPUnit-Aufruf im CI-Skript ist Pflicht. Ohne das Verzeichnis bricht PHPUnit mit einem Fatal Error beim Schreiben der Datei ab.2Warum zeigt GitHub Actions keine Testergebnisse?
upload-artifact. Für PR-Anzeige zusätzliche Actions wie dorny/test-reporter verwenden.3Warum ist die Coverage-Datei leer, obwohl Xdebug installiert ist?
XDEBUG_MODE=coverage als Umgebungsvariable. Ohne diese Variable ist Coverage deaktiviert, auch wenn Xdebug geladen ist.4Welches Coverage-Format für GitLab CI?
5Wie extrahiere ich den Coverage-Prozentsatz für einen GitLab Badge?
coverage: '/^\s*Lines:\s*\d+.\d+\%/' in der .gitlab-ci.yml. GitLab extrahiert den ersten Match aus der Job-Ausgabe automatisch.6Warum Coverage in separaten CI-Job auslagern?
7Wie finde ich die langsamsten Tests?
--order-by=duration. Alternativ das time-Attribut aus dem JUnit XML mit einem Shell-Skript auswerten und sortieren.8Was bedeutet if: always() bei GitHub Actions?
if: always() überspringt GitHub Actions den Upload-Step bei Fehlern. JUnit XML wird aber genau dann gebraucht. if: always() stellt sicher, dass die Datei immer hochgeladen wird.9Wie lange sollten Test-Artefakte aufbewahrt werden?
10Kann PHPUnit JUnit XML und Coverage gleichzeitig ausgeben?
<logging> und <coverage> mehrere Formate gleichzeitig konfiguriert werden. PHPUnit erzeugt alle konfigurierten Ausgaben in einem Lauf.