Automatisierte Tests gehören zu den wichtigsten Werkzeugen in der professionellen PHP-Entwicklung. Sie decken Fehler frühzeitig auf, sichern bestehende Funktionalität bei Änderungen ab und dokumentieren das erwartete Verhalten von Code. PHPUnit ist das etablierte Testing Framework für PHP und basiert auf der bewährten xUnit-Architektur. Dieses Tutorial zeigt Schritt für Schritt, wie PHPUnit installiert wird, wie Testklassen mit Assertions aufgebaut werden und wie sich fortgeschrittene Techniken wie Data Providers und Mocking einsetzen lassen.

Zunächst klären wir, was PHPUnit genau ist und wie es sich in den Entwicklungsalltag einfügt.
Was ist PHPUnit?
PHPUnit ist ein von Sebastian Bergmann entwickeltes Testing Framework für PHP. Es ermöglicht das Schreiben und Ausführen von Unit Tests, also automatisierten Prüfungen einzelner Code-Einheiten wie Funktionen, Methoden oder Klassen. Im Gegensatz zu Integration Tests oder funktionalen Tests konzentrieren sich Unit Tests auf isolierte Bausteine des Codes, ohne externe Abhängigkeiten wie Datenbanken oder APIs einzubeziehen.
PHPUnit stellt eine Vielzahl von Assertion-Methoden bereit, mit denen sich erwartete Ergebnisse gegen tatsächliche Werte prüfen lassen. Darüber hinaus bietet es Funktionen für Mocking, Data Providers und Code Coverage. Das Framework lässt sich nahtlos in CI/CD-Pipelines integrieren und unterstützt so die kontinuierliche Qualitätssicherung.
PHPUnit installieren
Die Installation von PHPUnit erfolgt über Composer als Dev-Abhängigkeit. Dadurch steht das Framework nur in der Entwicklungsumgebung zur Verfügung und wird nicht in die Produktionsumgebung ausgeliefert.
<?php
/* PHPUnit als Dev-Abhängigkeit installieren */
/* Terminal: composer require --dev phpunit/phpunit */
/* Verzeichnisstruktur nach der Installation */
/*
projekt/
+-- src/
| +-- Rechner.php
+-- tests/
| +-- RechnerTest.php
+-- composer.json
+-- phpunit.xml
*/
Nach der Installation wird eine Konfigurationsdatei phpunit.xml im Projektstammverzeichnis angelegt. Diese Datei legt fest, wo PHPUnit nach Testklassen suchen soll und welche Einstellungen gelten.
<?php
/* Inhalt der phpunit.xml Konfigurationsdatei */
/*
<phpunit bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="Unit">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
*/
Die Einstellung bootstrap sorgt dafür, dass der Composer-Autoloader geladen wird. Die testsuite verweist auf das Verzeichnis tests, in dem alle Testklassen liegen. Die aktuelle Version PHPUnit 11 setzt mindestens PHP 8.2 voraus. Daher sollte die PHPUnit-Version stets zur eingesetzten PHP-Version passen.
Den ersten Test schreiben
Testklassen in PHPUnit erben von PHPUnit\Framework\TestCase. Jede Testmethode beginnt mit dem Präfix test und gibt über Assertions an, welches Ergebnis erwartet wird. Als Grundlage dient eine einfache Rechner-Klasse.
<?php
/* Die zu testende Klasse */
class Rechner
{
public function addiere(int $a, int $b): int
{
return $a + $b;
}
public function subtrahiere(int $a, int $b): int
{
return $a - $b;
}
public function dividiere(int $a, int $b): float
{
if ($b === 0) {
throw new \DivisionByZeroError('Division durch Null');
}
return $a / $b;
}
}
Die Klasse bietet drei Methoden: addiere(), subtrahiere() und dividiere(). Die Methode dividiere() wirft bei einer Division durch Null eine Exception. Für diese Klasse wird nun eine Testklasse geschrieben.
<?php
use PHPUnit\Framework\TestCase;
class RechnerTest extends TestCase
{
public function testAddition(): void
{
$rechner = new Rechner();
$ergebnis = $rechner->addiere(2, 3);
$this->assertSame(5, $ergebnis);
}
public function testSubtraktion(): void
{
$rechner = new Rechner();
$ergebnis = $rechner->subtrahiere(10, 4);
$this->assertSame(6, $ergebnis);
}
public function testDivisionDurchNull(): void
{
$this->expectException(\DivisionByZeroError::class);
$rechner = new Rechner();
$rechner->dividiere(10, 0);
}
}
Die Methode testAddition() erzeugt eine Instanz der Klasse Rechner, ruft addiere(2, 3) auf und prüft mit assertSame(), ob das Ergebnis exakt 5 ist. Die Methode testDivisionDurchNull() verwendet expectException(), um sicherzustellen, dass die erwartete Exception geworfen wird. Der Test wird über die Kommandozeile mit vendor/bin/phpunit ausgeführt.
Assertions im Detail
Assertions sind das Herzstück jedes Unit Tests. Sie vergleichen einen erwarteten Wert mit dem tatsächlichen Ergebnis und melden einen Fehler, wenn die Werte nicht übereinstimmen. PHPUnit bietet zahlreiche Assertion-Methoden für unterschiedliche Prüfszenarien.
<?php
use PHPUnit\Framework\TestCase;
class AssertionBeispieleTest extends TestCase
{
public function testGleichheit(): void
{
/* assertEquals prüft mit Typumwandlung */
$this->assertEquals(5, '5');
/* assertSame prüft ohne Typumwandlung (strikt) */
$this->assertSame(5, 5);
}
public function testBoolescheWerte(): void
{
$this->assertTrue(str_contains('PHPUnit', 'PHP'));
$this->assertFalse(empty([1, 2, 3]));
}
public function testArrays(): void
{
$warenkorb = ['Laptop', 'Maus', 'Tastatur'];
$this->assertCount(3, $warenkorb);
$this->assertContains('Maus', $warenkorb);
$this->assertNotContains('Monitor', $warenkorb);
}
public function testTypen(): void
{
$rechner = new Rechner();
$this->assertInstanceOf(Rechner::class, $rechner);
$this->assertIsInt($rechner->addiere(1, 2));
$this->assertIsFloat($rechner->dividiere(10, 3));
}
}
Der Unterschied zwischen assertEquals() und assertSame() ist entscheidend. Während assertEquals() eine lose Prüfung mit Typumwandlung durchführt, vergleicht assertSame() sowohl Wert als auch Typ. In den meisten Fällen ist assertSame() die bessere Wahl, da sie unerwünschte Typumwandlungen aufdeckt.
Data Providers verwenden
Data Providers ermöglichen es, einen einzelnen Test mit verschiedenen Datensätzen auszuführen. Statt für jeden Testfall eine eigene Methode zu schreiben, liefert ein Data Provider die Eingabe- und Erwartungswerte als Array.
<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
class StringHelperTest extends TestCase
{
public static function slugProvider(): array
{
return [
'einfacher Text' => ['Hallo Welt', 'hallo-welt'],
'mehrere Worte' => ['PHP ist toll', 'php-ist-toll'],
'Sonderzeichen' => ['Preis: 10 Euro', 'preis-10-euro'],
'Grossbuchstaben' => ['UNIT TEST', 'unit-test'],
];
}
#[DataProvider('slugProvider')]
public function testSlugErzeugung(
string $eingabe,
string $erwartet
): void {
$this->assertSame(
$erwartet,
StringHelper::slug($eingabe)
);
}
}
Die statische Methode slugProvider() gibt ein Array zurück, dessen Einträge jeweils die Parameter für einen Testdurchlauf enthalten. Das Attribut #[DataProvider('slugProvider')] verknüpft den Provider mit der Testmethode. PHPUnit führt den Test dann für jeden Datensatz einzeln aus und zeigt bei einem Fehler an, welcher Datensatz fehlgeschlagen ist. Die benannten Schlüssel wie 'einfacher Text' erscheinen in der Testausgabe und erleichtern die Zuordnung.
Mocking und Test Doubles
Mocking ist eine Technik, um externe Abhängigkeiten in Unit Tests zu isolieren. Ein Mock Object ersetzt eine echte Klasse oder ein Interface und simuliert dessen Verhalten. So lassen sich Tests schreiben, die unabhängig von Datenbanken, APIs oder Dateisystemen funktionieren.
Das folgende Diagramm zeigt den Ablauf eines Tests mit Mock Object.
flowchart TD
A["Testmethode starten"] --> B["Mock Object erstellen"]
B --> C["Erwartungen definieren"]
C --> D["Mock in Klasse injizieren"]
D --> E["Methode ausführen"]
E --> F{"Assertions prüfen"}
F -->|Bestanden| G["Test erfolgreich"]
F -->|Fehlgeschlagen| H["Fehler melden"]
PHPUnit unterscheidet zwischen Stubs und Mock Objects. Ein Stub gibt vordefinierte Werte zurück, ohne das Aufrufverhalten zu prüfen. Ein Mock Object prüft zusätzlich, ob bestimmte Methoden mit bestimmten Parametern aufgerufen wurden.
<?php
use PHPUnit\Framework\TestCase;
interface MailerInterface
{
public function sende(string $nachricht): bool;
}
class BestellService
{
public function __construct(
private MailerInterface $mailer
) {}
public function bestellen(array $artikel): bool
{
if (count($artikel) === 0) {
return false;
}
$nachricht = 'Bestellung: ' . implode(', ', $artikel);
return $this->mailer->sende($nachricht);
}
}
class BestellServiceTest extends TestCase
{
public function testBestellungSendetEmail(): void
{
$mailer = $this->createMock(MailerInterface::class);
$mailer->expects($this->once())
->method('sende')
->with($this->stringContains('Bestellung'))
->willReturn(true);
$service = new BestellService($mailer);
$ergebnis = $service->bestellen(['Laptop', 'Maus']);
$this->assertTrue($ergebnis);
}
public function testLeereBestellungSendetKeineEmail(): void
{
$mailer = $this->createMock(MailerInterface::class);
$mailer->expects($this->never())
->method('sende');
$service = new BestellService($mailer);
$ergebnis = $service->bestellen([]);
$this->assertFalse($ergebnis);
}
}
Die Methode createMock() erzeugt ein Mock Object für das MailerInterface. Mit expects($this->once()) wird festgelegt, dass die Methode sende() genau einmal aufgerufen werden muss. Die Methode willReturn(true) definiert den Rückgabewert des Mocks. Im zweiten Test stellt expects($this->never()) sicher, dass bei einer leeren Bestellung keine E-Mail gesendet wird.
Code Coverage messen
Code Coverage zeigt an, welcher Anteil des Quellcodes durch Tests abgedeckt ist. PHPUnit kann diese Metrik automatisch erfassen und als Bericht ausgeben. Dafür wird eine PHP-Extension wie Xdebug oder PCOV benötigt.
Der Befehl vendor/bin/phpunit --coverage-text gibt eine Übersicht direkt in der Konsole aus. Für einen detaillierten HTML-Bericht eignet sich vendor/bin/phpunit --coverage-html coverage. Der Bericht zeigt zeilengenau, welche Codepfade durch Tests durchlaufen wurden und welche nicht. Eine hohe Code Coverage ist ein guter Indikator für die Testqualität, ersetzt jedoch nicht die inhaltliche Prüfung der Tests selbst. Auch 100 Prozent Abdeckung bedeutet nicht, dass alle Grenzfälle und Randbedingungen geprüft wurden. Die Testabdeckung dient vielmehr als Orientierung, um ungetestete Bereiche im Code systematisch zu identifizieren.
PHPUnit in die Entwicklungspraxis integrieren
PHPUnit entfaltet seinen vollen Nutzen erst durch die konsequente Einbindung in den Entwicklungsalltag. Moderne IDEs wie PhpStorm bieten eine direkte Integration, sodass Tests per Klick ausgeführt und Ergebnisse visuell dargestellt werden.
In CI/CD-Pipelines wie GitHub Actions oder GitLab CI wird PHPUnit bei jedem Push oder Pull Request automatisch ausgeführt. Ein fehlgeschlagener Test blockiert den Merge und verhindert, dass fehlerhafter Code in den Hauptzweig gelangt. Auch für kleine PHP-Projekte lohnt sich der Einsatz von PHPUnit, da selbst wenige Tests kritische Fehler zuverlässig aufdecken und das Vertrauen in den Code stärken.
Fazit
PHPUnit ist das Standardwerkzeug für Unit Testing in PHP. Die Installation über Composer ist unkompliziert, und die Konfiguration über phpunit.xml ermöglicht eine flexible Anpassung. Testklassen erben von TestCase und verwenden Assertions wie assertSame(), assertTrue() oder assertCount(), um erwartete Ergebnisse zu prüfen. Data Providers reduzieren Duplikation, indem ein einzelner Test mit mehreren Datensätzen ausgeführt wird. Mocking isoliert externe Abhängigkeiten und ermöglicht echte Unit Tests ohne Seiteneffekte. Code Coverage liefert Einblicke in die Testabdeckung und hilft, ungetestete Bereiche zu identifizieren. In Kombination mit CI/CD-Pipelines sorgt PHPUnit für kontinuierliche Qualitätssicherung und zuverlässigen PHP-Code.