von JUnit-Reports bis zur Matrixstrategie
Tests, die nur lokal ausgeführt werden, schützen das Projekt nicht. Erst die Integration in CI/CD macht Testautomatisierung zum Qualitätsgatter – jeder Pull Request wird automatisch geprüft, Coverage-Berichte werden als Artefakte gespeichert und Fehler werden dort gemeldet, wo sie entstehen: im Code, nicht erst beim Deploy. Dieser Artikel zeigt die vollständige CI/CD-Integration von PHPUnit für beide großen Plattformen.
Inhaltsverzeichnis
- 1. Das Grundprinzip: Tests als Pflichtbestandteil der Pipeline
- 2. GitLab CI: Unit-Tests konfigurieren
- 3. GitLab CI: Integrationstests mit Datenbankdienst
- 4. GitHub Actions: Unit-Tests konfigurieren
- 5. GitHub Actions: Matrixstrategie für mehrere PHP-Versionen
- 6. JUnit-Reports und Coverage-Berichte als Artefakte
- 7. Caching-Strategien für schnellere Pipelines
- 8. GitLab CI vs. GitHub Actions im Vergleich
- 9. Zusammenfassung
- 10. FAQ
1. Das Grundprinzip: Tests als Pflichtbestandteil der Pipeline
Eine CI/CD-Pipeline ohne automatisierte Tests ist ein Deployment-Mechanismus, kein Qualitätssicherungssystem. Der Unterschied liegt darin, dass eine Qualitätspipeline jeden Commit prüft, bevor er in den Hauptbranch oder in die Produktionsumgebung gelangt. PHPUnit-Tests sind das zentrale Werkzeug dieser Prüfung: Sie stellen sicher, dass jede Codeänderung das bestehende Verhalten nicht bricht und neue Funktionen das erwartete Verhalten zeigen.
Die Architektur einer PHPUnit-CI-Pipeline folgt in beiden Plattformen – GitLab CI und GitHub Actions – demselben Prinzip: Zuerst Abhängigkeiten installieren (Composer), dann Tests ausführen, dann Berichte speichern. Für größere Projekte kommt eine Stufenstruktur hinzu: statische Analyse zuerst (schnell und kein Datenbanksetup nötig), dann Unit-Tests, dann Integrationstests. Durch diese Reihenfolge schlagen Typfehler sofort fehl – nicht erst nach einem langen Datenbanksetup. Der Entwickler erhält schnelles, präzises Feedback.
2. GitLab CI: Unit-Tests konfigurieren
GitLab CI konfiguriert Pipelines über eine .gitlab-ci.yml-Datei im Projektstamm. Stages definieren die Ausführungsreihenfolge; Jobs innerhalb derselben Stage laufen parallel. Für PHPUnit-Unit-Tests wird ein Job in der test-Stage erstellt, der ein PHP-Image als Basis nimmt, Composer-Abhängigkeiten installiert und dann PHPUnit ausführt. Das --log-junit-Flag erzeugt eine JUnit-XML-Datei, die GitLab als Test-Report liest und im Merge-Request-Panel visualisiert.
Ein häufig übersehenes Detail: Der Composer-Cache sollte als GitLab-Cache konfiguriert werden, sodass Abhängigkeiten nicht bei jedem Job-Lauf neu heruntergeladen werden. Der Cache-Key basiert auf dem Hash der composer.lock-Datei – wenn sich keine Abhängigkeiten geändert haben, wird der gecachte vendor/-Ordner wiederverwendet und der Job startet erheblich schneller.
# .gitlab-ci.yml — PHPUnit Unit-Tests für PHP 8.4 Projekte
stages:
- static-analysis
- test
- report
variables:
COMPOSER_HOME: "${CI_PROJECT_DIR}/.composer"
XDEBUG_MODE: "off"
.php-base: &php-base
image: php:8.4-cli-alpine
before_script:
- apk add --no-cache git unzip curl
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install --prefer-dist --no-progress --no-interaction --optimize-autoloader
cache:
key: "composer-${CI_COMMIT_REF_SLUG}-${CI_PROJECT_ID}"
paths:
- .composer/
- vendor/
policy: pull-push
phpstan:
<<: *php-base
stage: static-analysis
script:
- vendor/bin/phpstan analyse --no-progress --memory-limit=512M
allow_failure: false
phpunit:unit:
<<: *php-base
stage: test
script:
- vendor/bin/phpunit --configuration phpunit.xml --testsuite unit
--log-junit var/log/tests/junit-unit.xml
--no-coverage
artifacts:
when: always
reports:
junit: var/log/tests/junit-unit.xml
expire_in: 7 days
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
3. GitLab CI: Integrationstests mit Datenbankdienst
Integrationstests, die eine Datenbank benötigen, erfordern in GitLab CI die Konfiguration eines Service – eines zusätzlichen Containers, der parallel zum Test-Job läuft. Der MySQL-Service wird in der Job-Konfiguration unter services: deklariert. GitLab CI stellt den Service-Container über den Hostnamen mysql erreichbar, der in den Datenbankverbindungsparametern verwendet wird.
Für Magento-Integrationstests kommen weitere Umgebungsvariablen hinzu: Datenbankname, Benutzer, Passwort, Hostname. Diese werden als CI/CD-Variablen in GitLab gespeichert und in der Pipeline als Umgebungsvariablen injiziert. Sensible Werte wie Datenbankpasswörter werden als masked und protected Variables definiert, sodass sie in Logs nicht erscheinen. Der Integrationstest-Job hängt explizit vom Unit-Test-Job ab (needs: [phpunit:unit]), sodass Integrationstests nur starten, wenn Unit-Tests grün sind.
# .gitlab-ci.yml — Integrationstests mit MySQL-Service
phpunit:integration:
stage: test
image: php:8.4-cli-alpine
needs:
- job: phpunit:unit
artifacts: false
services:
- name: mysql:8.0
alias: mysql
variables:
MYSQL_DATABASE: magento_test
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_USER: magento
MYSQL_PASSWORD: ${DB_PASSWORD}
DB_HOST: mysql
DB_NAME: magento_test
DB_USER: magento
DB_PASSWORD: ${DB_PASSWORD}
XDEBUG_MODE: "off"
before_script:
- apk add --no-cache git unzip curl mariadb-client
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install --prefer-dist --no-progress --no-interaction
- mysqladmin ping -h mysql --wait=30
- mysql -h mysql -u root -p${DB_ROOT_PASSWORD} ${MYSQL_DATABASE} < dev/tests/integration/db/schema.sql
script:
- vendor/bin/phpunit --configuration phpunit-integration.xml
--log-junit var/log/tests/junit-integration.xml
--no-coverage
artifacts:
when: always
reports:
junit: var/log/tests/junit-integration.xml
expire_in: 7 days
4. GitHub Actions: Unit-Tests konfigurieren
GitHub Actions konfiguriert Workflows über YAML-Dateien im Verzeichnis .github/workflows/. Ein Workflow besteht aus einem oder mehreren Jobs, die wiederum aus Steps bestehen. Für PHPUnit-Unit-Tests wird ein Workflow erstellt, der bei Push- und Pull-Request-Events ausgelöst wird, PHP mit der gewünschten Version installiert (über actions/setup-php), Composer-Abhängigkeiten installiert und PHPUnit ausführt.
Die actions/setup-php-Action aus dem Shivammathur-Repository ist der Standard für PHP-Setup in GitHub Actions. Sie installiert PHP in der gewünschten Version, konfiguriert Xdebug oder pcov und stellt alle gängigen Extensions bereit. Mit coverage: pcov in der Action-Konfiguration wird pcov automatisch aktiviert – keine manuelle Extension-Installation nötig. Das macht die Konfiguration deutlich kompakter als in GitLab CI mit einem reinen PHP-Alpine-Image.
# .github/workflows/phpunit.yml — Unit-Tests in GitHub Actions
name: PHPUnit Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
phpunit-unit:
name: "PHPUnit Unit Tests (PHP ${ { matrix.php } })"
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.3', '8.4']
fail-fast: false
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP ${ { matrix.php } }
uses: shivammathur/setup-php@v2
with:
php-version: ${ { matrix.php } }
extensions: mbstring, intl, gd, zip
coverage: pcov
ini-values: pcov.enabled=1, memory_limit=512M
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ~/.composer/cache
key: ${ { runner.os } }-composer-${ { hashFiles('**/composer.lock') } }
restore-keys: ${ { runner.os } }-composer-
- name: Install Composer dependencies
run: composer install --prefer-dist --no-progress --no-interaction
- name: Run PHPUnit unit tests
run: |
vendor/bin/phpunit \
--configuration phpunit.xml \
--testsuite unit \
--log-junit var/log/tests/junit-unit.xml \
--coverage-clover var/log/tests/coverage.xml \
--no-interaction
- name: Upload JUnit report
uses: actions/upload-artifact@v4
if: always()
with:
name: junit-unit-php${ { matrix.php } }
path: var/log/tests/junit-unit.xml
- name: Upload Coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-php${ { matrix.php } }
path: var/log/tests/coverage.xml
5. GitHub Actions: Matrixstrategie für mehrere PHP-Versionen
Die Matrixstrategie in GitHub Actions ermöglicht es, denselben Job gleichzeitig mit verschiedenen Konfigurationen auszuführen. Für PHP-Projekte ist die häufigste Anwendung das parallele Testen gegen mehrere PHP-Versionen: PHP 8.3 und PHP 8.4 in einem Workflow, der bei einem Push beide Tests gleichzeitig startet. Das stellt sicher, dass der Code mit der aktuellen und der nächsten PHP-Version kompatibel ist.
Die Matrixstrategie lässt sich erweitern: Neben PHP-Versionen können auch verschiedene Datenbankversionen (MySQL 8.0 vs. MariaDB 10.6) oder Betriebssystemvarianten (ubuntu vs. macos) in die Matrix aufgenommen werden. Mit fail-fast: false werden alle Matrix-Jobs zu Ende ausgeführt, auch wenn einer fehlschlägt. Das gibt vollständige Auskunft darüber, welche Kombinationen funktionieren – nicht nur, ob die erste fehlgeschlagene Kombination ein Problem hat. Für Produktionsprojekte ist fail-fast: false bei der Test-Matrix empfohlen; für die statische Analyse kann fail-fast: true sinnvoll sein.
6. JUnit-Reports und Coverage-Berichte als Artefakte
JUnit-XML ist ein standardisiertes Format für Test-Reports, das beide Plattformen direkt unterstützen. GitLab CI liest JUnit-XML über den artifacts.reports.junit-Schlüssel und visualisiert die Ergebnisse im Merge-Request-Panel: fehlgeschlagene Tests werden mit ihren Fehlermeldungen angezeigt, ohne dass man die Pipeline-Logs durchsuchen muss. GitHub Actions zeigt JUnit-Reports im Check-Panel des Pull Requests, wenn eine entsprechende Action verwendet wird (z.B. actions/upload-artifact kombiniert mit einem Test-Reporter-Action).
Coverage-Berichte als Artefakte gespeichert ermöglichen die Trendverfolgung: Wie hat sich die Abdeckung im Laufe der Zeit verändert? GitLab CI kann aus der Coverage-Ausgabe des Tests direkt einen Prozentwert extrahieren (mit dem coverage-Schlüssel und einem regulären Ausdruck) und diesen im Merge-Request-Widget anzeigen. Ein einfacher Regex wie /^\s*Lines:\s*\d+.\d+\%/ extrahiert den Wert aus PHPUnit-Textausgabe. GitHub Actions kann Coverage-Berichte über Actions wie codecov/codecov-action an Codecov oder SonarCloud weitergeben.
| Feature | GitLab CI | GitHub Actions | Hinweis |
|---|---|---|---|
| JUnit-Reports | Nativ (artifacts.reports.junit) | Via Action (upload-artifact) | GitLab visualisiert direkt im MR |
| Coverage-Widget | Nativ (coverage-Regex) | Via codecov oder SonarCloud | GitLab einfacher zu konfigurieren |
| Datenbankdienst | Services-Schlüssel | Services in Job-Config | Beide gleich einfach |
| Matrixstrategie | Parallel-Keyword | Nativ (matrix) | GitHub Actions eleganter |
| PHP-Setup | PHP-Docker-Image manuell | setup-php Action | GitHub Actions bequemer |
7. Caching-Strategien für schnellere Pipelines
Das Caching von Composer-Abhängigkeiten ist die wichtigste einzelne Maßnahme, um CI-Pipeline-Laufzeiten zu reduzieren. Composer installiert typischerweise hunderte von Packages und kann mehrere Minuten dauern, wenn alles neu heruntergeladen werden muss. Mit korrektem Caching reduziert sich dieser Schritt auf Sekunden, weil nur geänderte oder neue Packages heruntergeladen werden müssen. Beide Plattformen unterstützen Cache-Keys basierend auf Datei-Hashes – wenn sich composer.lock nicht geändert hat, wird der gespeicherte Cache verwendet.
Neben Composer-Paketen lohnt es sich, den PHPUnit-Result-Cache (.phpunit.cache/) zwischen Pipeline-Runs zu cachen. Dieser Cache enthält Informationen über zuletzt fehlgeschlagene Tests und ermöglicht es, bei wiederholten Runs zuerst die Fehler-Tests auszuführen. In GitLab CI wird der Cache über den cache:-Schlüssel mit einer passenden Policy konfiguriert; in GitHub Actions über actions/cache. Ein weiterer Caching-Kandidat in Magento-Projekten: Das generierte Code-Verzeichnis (generated/), das bei jedem Build neu erstellt wird und mehrere Sekunden kostet.
# .github/workflows/phpunit.yml — Vollständige Integration mit Services und Caching
jobs:
phpunit-integration:
name: "PHPUnit Integration Tests"
runs-on: ubuntu-latest
needs: phpunit-unit
services:
mysql:
image: mysql:8.0
env:
MYSQL_DATABASE: magento_test
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_USER: magento
MYSQL_PASSWORD: magento
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP 8.4
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: mbstring, intl, pdo_mysql
coverage: none # No coverage for integration tests in CI
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ~/.composer/cache
key: ${ { runner.os } }-composer-${ { hashFiles('**/composer.lock') } }
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Wait for MySQL and import schema
run: |
mysql -h 127.0.0.1 -u root -prootpassword magento_test \
< dev/tests/integration/db/schema.sql
- name: Run integration tests
env:
DB_HOST: 127.0.0.1
DB_NAME: magento_test
DB_USER: magento
DB_PASSWORD: magento
XDEBUG_MODE: "off"
run: |
vendor/bin/phpunit \
--configuration phpunit-integration.xml \
--log-junit var/log/tests/junit-integration.xml \
--no-interaction
- name: Upload JUnit report
uses: actions/upload-artifact@v4
if: always()
with:
name: junit-integration
path: var/log/tests/junit-integration.xml
9. Zusammenfassung
Die Integration von PHPUnit in GitLab CI und GitHub Actions folgt demselben Grundprinzip, unterscheidet sich aber in den Details. Beide Plattformen unterstützen JUnit-XML-Reports, Artefaktspeicherung, Datenbankdienste und Caching. GitHub Actions bietet mit der Matrixstrategie und der setup-php-Action etwas bequemere PHP-Konfiguration; GitLab CI integriert JUnit-Reports und Coverage-Widgets nativ im Merge-Request-Panel ohne zusätzliche Drittanbieter-Integrationen.
Die wichtigsten Konfigurationsprinzipien für beide Plattformen: Composer-Abhängigkeiten cachen, JUnit-XML als Artefakt speichern, Unit- und Integrationstests in separate Jobs aufteilen, statische Analyse vor Tests ausführen und Coverage nur bei Bedarf (nicht in jedem Feature-Branch-Build) messen. Eine sauber strukturierte CI-Pipeline mit diesen Maßnahmen gibt Entwicklern schnelles, präzises Feedback bei jedem Commit und macht Qualitätsprüfung zur unsichtbaren, selbstverständlichen Routine.
PHPUnit in CI/CD — Das Wichtigste auf einen Blick
GitLab CI JUnit
artifacts.reports.junit liest die XML-Datei und visualisiert Ergebnisse direkt im Merge-Request-Panel. coverage-Regex extrahiert den Prozentwert.
GitHub Actions
setup-php Action für einfaches PHP-Setup mit pcov. Matrixstrategie für parallele Tests auf mehreren PHP-Versionen. actions/upload-artifact für Reports.
Caching
Composer-Cache mit hashFiles(composer.lock) als Key. PHPUnit-Result-Cache für defects-Reihenfolge. Spart Minuten pro Pipeline-Lauf.
Pipeline-Struktur
Statische Analyse → Unit-Tests → Integrationstests. Jede Stage kann nur starten, wenn die vorherige erfolgreich war. Coverage nur in dedizierten Jobs.