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.
Inhaltsverzeichnis
- 1. Warum das Setup entscheidend ist
- 2. PHPUnit per Composer installieren
- 3. Autoloading richtig konfigurieren
- 4. Die Bootstrap-Datei verstehen und schreiben
- 5. phpunit.xml Schritt für Schritt aufbauen
- 6. Test-Suites: Unit, Integration, Functional trennen
- 7. Den ersten Test korrekt schreiben
- 8. Code-Coverage-Konfiguration
- 9. Konfigurationsansätze im Vergleich
- 10. Zusammenfassung
- 11. FAQ
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.