@test
assert
PHPUnit · Composer · Testing · Setup
PHPUnit sauber einrichten
Composer, Bootstrap, phpunit.xml und Test-Suites

Ein halbherzig eingerichtetes PHPUnit-Projekt verlangsamt jeden Entwickler-Workflow: fehlende Autoload-Pfade, fehlendes Bootstrap, keine Trennung von Unit- und Integrationstests. Dieses Tutorial zeigt, wie ein sauberes Setup von Anfang an aussieht – reproduzierbar, teamfähig und CI-ready.

12 Min. Lesezeit Composer · phpunit.xml · Bootstrap · Test-Suites · Autoloading PHPUnit 10/11 · PHP 8.2+

1. Warum das Setup entscheidend ist

PHPUnit ist in wenigen Minuten installiert – aber ein sauberes PHPUnit-Setup ist eine andere Sache. Ohne klar definierte Test-Suites laufen Unit-Tests und Integrationstests gemischt, was die Ausführungszeit unnötig verlängert und lokales Feedback verlangsamt. Ohne Bootstrap-Datei fehlen Umgebungsvariablen oder der Autoloader, sodass Tests nur in bestimmten Kontexten laufen. Ohne strukturierte phpunit.xml erzwingt jeder Teamkollege seine eigenen Kommandozeilenargumente.

Ein sauber aufgesetztes Projekt dagegen ermöglicht es, mit einem einzelnen vendor/bin/phpunit --testsuite unit nur die schnellen Unit-Tests zu laufen, mit einem zweiten Befehl die Integrationstests gegen eine echte Datenbank. Die CI-Pipeline nutzt dieselbe Konfigurationsdatei, sodass lokal und remote identische Bedingungen herrschen. Der Aufwand für das initiale Setup zahlt sich nach dem zweiten Teamkollegen bereits aus.

Besonders in PHP-8-Projekten mit Strict Types und Constructor Property Promotion ist ein strukturiertes Test-Setup kein Luxus, sondern Grundlage für Refactoring-Sicherheit. Wer nach einer Typsignatur-Änderung nicht sofort weiß, ob alle Aufrufer noch korrekt sind, hat kein schlechtes Testing – er hat kein Testing mit dem richtigen Scope.

2. PHPUnit per Composer installieren

PHPUnit gehört ausschließlich in die require-dev-Sektion der composer.json. Produktions-Dependencies werden von Test-Dependencies strikt getrennt. In CI-Deployments kann man dann composer install --no-dev verwenden, um den Produktions-Build schlank zu halten. Der korrekte Befehl für die Installation ist composer require --dev phpunit/phpunit ^11. PHPUnit 11 setzt PHP 8.2 voraus, PHPUnit 10 ist noch mit PHP 8.1 kompatibel.

Nach der Installation legt Composer das ausführbare Skript unter vendor/bin/phpunit ab. In einer teamweiten Umgebung empfiehlt es sich, diesen Pfad in Makefiles oder Shell-Wrappern zu abstrahieren, sodass niemand lokal eine globale PHPUnit-Installation benötigt. Global installierte PHPUnit-Versionen führen regelmäßig zu Versionskonflikten zwischen Projekten – ein häufiger Grund für Tests, die lokal grün sind und remote rot.

Neben PHPUnit selbst lohnt es sich, weitere Test-Hilfspakete direkt einzurichten: phpunit/php-code-coverage für Coverage-Reports (wird mit PHPUnit 11 automatisch mitinstalliert), fakerphp/faker für Testdaten-Generierung, und bei Bedarf mockery/mockery als Alternative zur eingebauten Mock-API. Diese Pakete alle als require-dev zu deklarieren hält die Produktions-Deployment-Größe minimal.


{
  "name": "mironsoft/shop",
  "require": {
    "php": ">=8.2",
    "magento/product-community-edition": "2.4.8"
  },
  "require-dev": {
    "phpunit/phpunit": "^11",
    "fakerphp/faker": "^1.23",
    "mockery/mockery": "^1.6"
  },
  "autoload": {
    "psr-4": {
      "Mironsoft\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "Mironsoft\\Tests\\": "tests/"
    }
  },
  "scripts": {
    "test": "vendor/bin/phpunit",
    "test:unit": "vendor/bin/phpunit --testsuite unit",
    "test:integration": "vendor/bin/phpunit --testsuite integration",
    "test:coverage": "vendor/bin/phpunit --coverage-html var/coverage"
  }
}

3. Autoloading richtig konfigurieren

Das häufigste Problem nach der PHPUnit-Installation: Klassen werden im Test nicht gefunden, weil der Autoloader nicht korrekt eingerichtet ist. Composer nutzt PSR-4-Autoloading, was bedeutet, dass Namespace und Verzeichnisstruktur exakt übereinstimmen müssen. Der Produktionscode liegt unter src/ mit dem Namespace-Präfix Mironsoft\\, der Testcode liegt unter tests/ mit dem Präfix Mironsoft\\Tests\\.

Der kritische Unterschied: Produktions-Autoloading kommt in autoload, Test-Autoloading in autoload-dev. Das verhindert, dass Testklassen in den optimierten Autoloader des Produktionsbuilds aufgenommen werden. Nach jeder Änderung der composer.json muss composer dump-autoload ausgeführt werden, damit die Autoload-Dateien unter vendor/composer/ aktualisiert werden.

In Magento-2-Projekten ist das Autoloading komplexer: Magento registriert seinen eigenen Autoloader, der mit dem Composer-Autoloader zusammenarbeitet. Unit-Tests ohne Magento-Bootstrap können den reinen Composer-Autoloader verwenden. Integrationstests, die Magento-Klassen benötigen, müssen den Magento-Autoloader über das Bootstrap initialisieren.

4. Die Bootstrap-Datei verstehen und schreiben

Die Bootstrap-Datei wird einmalig vor dem ersten Test ausgeführt. Sie ist der richtige Ort für alles, was die gesamte Test-Suite benötigt: Composer-Autoloader einbinden, Umgebungsvariablen aus einer .env.test-Datei laden, Zeitzone setzen, Test-Datenbankverbindungen initialisieren. Eine typische Bootstrap-Datei für ein PHP-Projekt ohne Framework ist minimal – drei bis zehn Zeilen. Eine Bootstrap für Integrationstests gegen eine Datenbank kann deutlich umfangreicher sein.

Der häufige Fehler: alles in die Bootstrap zu packen, was eigentlich ins setUp() der Testklassen gehört. Die Bootstrap läuft einmal pro Testsuite-Ausführung, nicht pro Testklasse oder Testmethode. Datenbankverbindungen, die in der Bootstrap geöffnet werden, bleiben für alle Tests offen – was bei paralleler Ausführung zu Konflikten führen kann. Datenbankfixtures, Test-Transaktionen und Scope-spezifische Initialisierung gehören in setUp() und tearDown().


<?php

declare(strict_types=1);

// tests/bootstrap.php — Test bootstrap for PHPUnit
// Loaded once before the first test in the suite

// 1. Composer autoloader
$autoloader = require dirname(__DIR__) . '/vendor/autoload.php';

// 2. Load test environment variables (not committed to VCS)
if (file_exists(dirname(__DIR__) . '/.env.test')) {
    $lines = file(dirname(__DIR__) . '/.env.test', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach ($lines as $line) {
        if (str_starts_with(trim($line), '#') || !str_contains($line, '=')) {
            continue;
        }
        [$key, $value] = explode('=', $line, 2);
        $_ENV[trim($key)] = trim($value);
        putenv(trim($key) . '=' . trim($value));
    }
}

// 3. Set default timezone to avoid date() warnings in tests
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Berlin');

// 4. Ensure error reporting is strict in test environment
error_reporting(E_ALL);
ini_set('display_errors', '1');

5. phpunit.xml Schritt für Schritt aufbauen

Die phpunit.xml ist die zentrale Konfigurationsdatei für PHPUnit. Sie definiert, welche Test-Dateien geladen werden, welche Suites existieren, welche Bootstrap-Datei verwendet wird, und wie Coverage-Berichte erstellt werden. PHPUnit sucht diese Datei im Projekt-Root. Eine alternative Datei kann mit dem --configuration-Flag angegeben werden, was für unterschiedliche CI-Umgebungen nützlich ist.

Ab PHPUnit 10 ist das XML-Schema strenger. Veraltete Attribute wie verbose direkt im <phpunit>-Element wurden entfernt. Die Migration von PHPUnit 9 auf 10 oder 11 beginnt daher immer mit der Validierung der phpunit.xml gegen das neue Schema: vendor/bin/phpunit --migrate-configuration konvertiert die meisten veralteten Attribute automatisch. Was danach noch rot markiert ist, muss manuell angepasst werden.

Die Coverage-Konfiguration innerhalb der phpunit.xml bestimmt, welche Quelldateien in den Coverage-Report aufgenommen werden. Das <source>-Element gibt das Verzeichnis des zu analysierenden Produktionscodes an. Ohne dieses Element misst PHPUnit nur Coverage für den tatsächlich ausgeführten Code, nicht die Lücken zu nicht ausgeführten Klassen – das führt zu trügerisch hohen Coverage-Werten.


<?xml version="1.0" encoding="UTF-8"?>
<!-- phpunit.xml — PHPUnit 11 configuration -->
<phpunit
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd"
  bootstrap="tests/bootstrap.php"
  colors="true"
  cacheDirectory=".phpunit.cache"
  executionOrder="depends,defects"
  failOnWarning="true"
  failOnRisky="true"
>
  <testsuites>
    <testsuite name="unit">
      <directory>tests/Unit</directory>
    </testsuite>
    <testsuite name="integration">
      <directory>tests/Integration</directory>
    </testsuite>
    <testsuite name="functional">
      <directory>tests/Functional</directory>
    </testsuite>
  </testsuites>

  <source>
    <include>
      <directory suffix=".php">src</directory>
    </include>
    <exclude>
      <directory>src/generated</directory>
    </exclude>
  </source>

  <coverage>
    <report>
      <html outputDirectory="var/coverage/html"/>
      <clover outputFile="var/coverage/clover.xml"/>
    </report>
  </coverage>

  <php>
    <env name="APP_ENV" value="test"/>
    <env name="DB_NAME" value="shop_test"/>
  </php>
</phpunit>

6. Test-Suites: Unit, Integration, Functional trennen

Die Trennung in Test-Suites ist keine Konvention, sondern eine produktive Notwendigkeit. Unit-Tests testen einzelne Klassen isoliert, ohne Datenbankverbindungen, HTTP-Anfragen oder Dateisystem-Zugriffe. Sie laufen in Millisekunden und geben sofortiges Feedback. In einem gesunden Projekt sind Unit-Tests die größte Suite – hunderte oder tausende schnelle Tests, die nach jeder Code-Änderung ausgeführt werden können.

Integrationstests testen das Zusammenspiel von Klassen, oft gegen eine echte Testdatenbank. Sie sind deutlich langsamer und werden seltener ausgeführt – typischerweise vor jedem Commit oder in der CI-Pipeline. Functional Tests (auch End-to-End-Tests) testen die Anwendung von außen, etwa über HTTP-Anfragen oder Browser-Automatisierung. Diese Suite läuft am langsamsten und meist nur in vollständigen CI-Pipelines.

Die Verzeichnisstruktur spiegelt diese Trennung wider: tests/Unit/, tests/Integration/, tests/Functional/. Innerhalb jeder Suite spiegelt die Verzeichnishierarchie die Namespace-Struktur des Produktionscodes. Eine Klasse Mironsoft\Catalog\PriceCalculator bekommt ihren Unit-Test unter tests/Unit/Catalog/PriceCalculatorTest.php – so ist die Zuordnung immer eindeutig und automatisch auffindbar.

7. Den ersten Test korrekt schreiben

Ein PHPUnit-Test ist eine Klasse, die von PHPUnit\Framework\TestCase erbt. Der Klassenname endet auf Test, Testmethoden beginnen mit test oder sind mit dem #[Test]-Attribut (ab PHPUnit 10) annotiert. PHP-8-Attribute haben die @test-Docblock-Annotation vollständig ersetzt – wer noch @test in neuen Tests schreibt, sollte das als Tech Debt behandeln.

Ein guter Unit-Test folgt dem AAA-Muster: Arrange (Testgegenstände vorbereiten), Act (die zu testende Methode aufrufen), Assert (das Ergebnis prüfen). Jede Testmethode testet genau eine Verhaltensweise. Tests mit mehreren unabhängigen Assertions, die unterschiedliche Verhaltensweisen prüfen, sollten aufgeteilt werden. Der Testname beschreibt die getestete Verhaltensweise in Klartext, zum Beispiel testCalculatesTotalPriceWithDiscount().

8. Code-Coverage-Konfiguration

Code-Coverage zeigt, welche Zeilen des Produktionscodes während der Tests ausgeführt wurden. PHPUnit unterstützt Xdebug (am verbreitetsten), PCOV (schneller für reine Coverage-Messungen) und den phpdbg-Interpreter. Für lokale Entwicklung mit PhpStorm ist Xdebug die natürliche Wahl, da es denselben Debugger verwendet. Für CI-Pipelines, wo nur Coverage gemessen werden soll, ist PCOV deutlich schneller.

Der Coverage-Report sollte nicht die einzige Qualitätsmessgröße sein. Eine hohe Coverage ohne aussagekräftige Assertions ist wertlos – Tests, die Code ausführen, aber nichts behaupten, erhöhen die Coverage-Zahl, ohne die Korrektheit des Codes zu prüfen. Sinnvoller ist eine Kombination aus Mutation-Testing (z.B. mit Infection PHP) und Coverage-Schwellwerten: failOnLowCoverage in der CI-Pipeline setzt einen Mindest-Coverage-Wert und verhindert Coverage-Rückschritte.

9. Konfigurationsansätze im Vergleich

Es gibt verschiedene Ansätze, PHPUnit zu konfigurieren. Der direkte Vergleich zeigt, welcher Ansatz für welches Szenario geeignet ist.

Aspekt Ohne Setup-Datei Mit phpunit.xml Vorteil
Test-Suites Alle Tests laufen immer --testsuite unit/integration Schnelles lokales Feedback
Bootstrap Manuell per --bootstrap Automatisch geladen Kein Vergessen möglich
Coverage Kein Source-Filter <source> exakt definiert Reale Coverage ohne Verfälschung
Umgebungsvariablen Systemweit oder .env hacken <php><env> in phpunit.xml Reproduzierbar, teamweit gleich
Reihenfolge Dateisystem-Reihenfolge executionOrder="depends,defects" Fehlgeschlagene Tests zuerst

Mironsoft

PHPUnit-Setup, Test-Architektur und CI-Integration für PHP-Projekte

PHPUnit professionell aufsetzen?

Wir richten PHPUnit korrekt ein – mit sauberem Bootstrap, strukturierten Test-Suites, Coverage-Konfiguration und CI-Integration. Kein manuelles Debuggen von Autoload-Problemen mehr.

Setup-Review

Analyse des bestehenden PHPUnit-Setups auf typische Konfigurationsprobleme

Suite-Architektur

Unit-, Integrations- und Funktionstests sauber trennen und strukturieren

CI-Integration

PHPUnit in GitHub Actions, GitLab CI oder Jenkins mit Coverage-Reports einbinden

10. Zusammenfassung

Ein sauberes PHPUnit-Setup besteht aus vier Kernelementen: korrekt aufgeteiltem Autoloading in composer.json, einer Bootstrap-Datei die genau das Notwendige initialisiert, einer validen phpunit.xml mit Source-Filter und Suites, sowie einer Verzeichnisstruktur, die Unit-, Integrations- und Funktionstests klar voneinander trennt. Diese vier Elemente ermöglichen reproduzierbare Testläufe für jeden Entwickler und jede CI-Umgebung ohne manuelle Konfigurationsarbeit.

Der häufigste Fehler beim PHPUnit-Setup ist das Mischen von Verantwortlichkeiten: zu viel Logik in der Bootstrap, keine Trennung der Test-Suites, fehlende autoload-dev-Sektion in Composer. Diese Fehler rächen sich spätestens dann, wenn das Projekt wächst und die Test-Suite ohne Suitentrennung Minuten statt Sekunden braucht. Ein sauberes initiales Setup kostet zwei Stunden und spart danach täglich Zeit.

PHPUnit sauber einrichten — Das Wichtigste auf einen Blick

Composer

PHPUnit nur in require-dev. autoload und autoload-dev trennen. Composer-Scripts für test, test:unit, test:integration definieren.

Bootstrap

Nur globale Initialisierung: Autoloader, Umgebungsvariablen, Zeitzone. Kein Test-Setup, das in setUp() gehört.

phpunit.xml

Schema-Version korrekt, source-Element für Coverage, executionOrder="depends,defects" für schnelleres Debugging.

Test-Suites

Unit in Millisekunden, Integration gegen Testdatenbank, Functional für End-to-End. Getrennte Verzeichnisse, getrennte Laufzeiten.

11. FAQ: PHPUnit sauber einrichten

1Welche PHPUnit-Version für PHP 8.2?
PHPUnit 11 für PHP 8.2 und 8.3. PHPUnit 10 für PHP 8.1+. PHPUnit 9 nicht mehr für neue Projekte – keine aktive Weiterentwicklung.
2Was gehört in die Bootstrap-Datei?
Nur globale Initialisierung: Autoloader, Umgebungsvariablen, Zeitzone. Test-spezifisches Setup gehört in setUp().
3Wie trenne ich Unit- und Integrationstests?
Getrennte Verzeichnisse (tests/Unit, tests/Integration) und separate testsuites in phpunit.xml. Unit ohne externe Abhängigkeiten, Integration darf gegen Datenbank testen.
4Warum zeigt Coverage falsche Werte an?
Ohne source-Element werden nicht ausgeführte Klassen nicht gezählt. source auf das src-Verzeichnis setzen für realistische Werte.
5@test vs. #[Test]-Attribut?
#[Test] ist die moderne PHP-8-Variante und ersetzt @test vollständig. Für alle neuen Tests ausschließlich das Attribut verwenden.
6Muss phpunit.xml ins Repository?
Ja. phpunit.xml definiert die gemeinsame Test-Konfiguration. phpunit.xml.dist als Vorlage plus gitignore-te phpunit.xml für lokale Überschreibungen ist ebenfalls gängig.
7Was bedeutet executionOrder="depends,defects"?
Zuletzt fehlgeschlagene Tests werden zuerst ausgeführt. Nach einer Codeänderung sofort sehen ob der Regressionstest jetzt grün ist.
8Xdebug oder PCOV für Coverage?
Xdebug für lokale Entwicklung mit PhpStorm-Debugging. PCOV für CI-Pipelines – 3–5x schneller, kein Debugging-Overhead.
9Testklassen aus Produktions-Autoloader heraushalten?
autoload-dev statt autoload in composer.json. composer install --no-dev im Produktions-Deployment ausführen.
10Migration von PHPUnit 9 auf 11?
vendor/bin/phpunit --migrate-configuration konvertiert phpunit.xml automatisch. Danach @annotation-Docblocks durch #[Attribute] ersetzen.