PHPUnit in GitLab CI und GitHub Actions integrieren
AI generated
@test
assert
PHPUnit · GitLab CI · GitHub Actions · JUnit · Coverage
PHPUnit in GitLab CI und GitHub Actions
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.

20 Min. Lesezeit GitLab CI · GitHub Actions · JUnit · Coverage · Caching · Matrix PHP 8.x · PHPUnit 10/11 · GitLab 17+ · GitHub Actions

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.