Unit-Tests gehören zum guten Stil moderner PHP-Entwicklung. Sie dokumentieren, was eine Klasse
tun soll, warnen dich beim nächsten Refactoring, wenn etwas kaputtgegangen ist, und zwingen dich
beim Schreiben dazu, deine Klassen so zu bauen, dass sie überhaupt testbar bleiben. Wer mit
PHPUnit
noch nicht gearbeitet hat, findet dort einen ausführlichen Einstieg.
Trotzdem ist gerade das Anlegen der ersten Tests oft eine mühsame Schreibarbeit. Eine Testklasse
erbt von TestCase, das setUp() muss die Konstruktor-Abhängigkeiten der
Klasse vorbereiten, und für jede Methode kommt eine eigene test*-Funktion. Das Tool
auf dieser Seite nimmt dir genau diese Boilerplate ab und liefert dir ein einsatzbereites Test-Skelett,
in das du nur noch die konkreten Erwartungen eintragen musst.
So benutzt du den Generator: Füge deinen PHP-Quellcode in das Eingabefeld ein, wähle die
passenden Optionen (PHPUnit oder Codeception, Mocks ja oder nein, Data-Provider mitgenerieren) und
klicke auf Tests generieren. Das Ergebnis kannst du direkt in die Zwischenablage kopieren,
als .php-Datei herunterladen oder in einem von bis zu 20 lokalen Projekt-Slots ablegen,
um später daran weiterzuarbeiten. Die Slot-Sammlung lässt sich als JSON-Datei
exportieren und importieren, sodass du sie zwischen Browsern oder Rechnern mitnehmen kannst.
Glossar: Fachbegriffe kurz erklärt
SUT (System Under Test)
Die Klasse, die gerade getestet wird. Im generierten Code heißt die Variable $this->sut (mit Konstruktor-Abhängigkeiten) oder $sut (ohne Abhängigkeiten).
Mock
Eine Test-Attrappe einer echten Klasse. Verhält sich vorhersagbar, statt echte Aufrufe an Datenbank, Filesystem oder API zu machen. PHPUnit erzeugt Mocks mit $this->createMock(Klasse::class).
Stub
Vereinfachter Mock, der nur vordefinierte Rückgabewerte liefert, ohne die Aufrufe selbst zu prüfen. In PHPUnit technisch über denselben Mock-Builder umgesetzt.
Assertion
Eine Prüfung, die der Test über das Ergebnis aufstellt. Beispiel: assertSame(5, $result) schlägt fehl, wenn $result nicht exakt der Integer 5 ist.
Data Provider
Eine statische Methode, die mehrere Eingabe-Sätze für denselben Test liefert. So prüfst du dieselbe Logik mit verschiedenen Werten, ohne die Testmethode zu duplizieren.
Arrange / Act / Assert (AAA)
Test-Aufbau in drei Phasen: Arrange bereitet Daten und Abhängigkeiten vor, Act ruft die zu testende Methode auf, Assert prüft das Ergebnis. Der Generator setzt diese Kommentare automatisch.
Test-Stub / Test-Skelett
Der vom Generator erzeugte Grundgerüst-Test mit // TODO-Markern. Erspart dir Boilerplate, du ergänzt nur noch die konkreten Erwartungen.
Coverage / #[CoversClass]
PHPUnit kann zählen, welche Klasse durch welchen Test abgedeckt wird. Das PHP-Attribut #[CoversClass(...)] (PHPUnit 10+) oder der PHPDoc-Tag @covers (PHPUnit 9) erklärt PHPUnit explizit, welche Klasse dieser Test prüfen soll. Pest nutzt dafür die globale Funktion covers(Klasse::class).
Eingabe
Tipp: Beispielklasse mit dem Button "Beispiel laden" einfügen. Mit Strg+Enter (bzw. Cmd+Enter) im Eingabefeld direkt Tests generieren.
Optionen
Tipp: Bewege die Maus über eine Option, um eine kurze Erklärung zu sehen.
Projekt-Slots
Speichere mehrere Klassen-/Test-Paare lokal im Browser. Es werden bis zu 20 Slots unterstützt.
Mit Export sicherst du alle Slots als JSON-Datei, mit Import liest du sie auf einem anderen
Rechner oder in einem anderen Browser wieder ein.
Generierte Tests
Was der Generator aus deinem Code herausliest
Der Generator läuft komplett im Browser, dein Code geht an keinen Server. Es kommt kein
vollwertiger PHP-Parser zum Einsatz, sondern ein abgespeckter Token-Scanner. Der deckt die typischen
Strukturen moderner PHP-Klassen zuverlässig ab:
Namespace
und Klassenname, auch in den Varianten final class,
abstract class
sowie readonly class (auch in Kombinationen wie final readonly class).
Genauso werden Enums erkannt, sowohl die einfache Form enum Status als auch
Backed Enums wie enum Status: string. Methoden auf Enums werden über die erste
gefundene case als $sut = EnumName::FirstCase; getestet.
Konstruktor inklusive
Constructor Promotion.
Aus public function __construct(private UserRepository $users) erkennt der Scanner,
dass UserRepository eine Abhängigkeit der Klasse ist, und richtet dafür im
setUp() automatisch einen Mock ein.
Methodensignaturen mit
Sichtbarkeit
(public, protected, private) und static-Modifier.
Statische Methoden werden anschließend korrekt über Klasse::methode()
aufgerufen, nicht über eine Instanz. Bei static in Kombination mit Reflection
erhält $ref->invoke() als erstes Argument null statt der SUT.
Parameter und
Return-Type-Hints,
inklusive nullable Varianten (?int, ?array), Union-Types und
Variadic-Parameter (string ...$tags). Variadic-Argumente werden im Test-Aufruf
mit zwei Beispielwerten gespiegelt, damit klar wird, dass die Methode mehrere Werte annehmen kann.
Skalare Default-Werte, die direkt in die Beispiel-Aufrufe der generierten Tests einfließen
PHPDoc-@throws oberhalb einer Methode. Der erste gefundene Exception-Typ
wird in $this->expectException(...) umgesetzt, die Rückgabe-Zuweisung
entfällt und der Standard-Assert-Block wird unterdrückt. Weitere
@throws-Einträge werden als Kommentar angehängt, weil
expectException() sich nicht stapeln lässt und jeder weitere Aufruf den
vorherigen überschreiben würde.
Findet der Scanner mehrere Klassen oder Enums in derselben Datei, wählt er den ersten Typ
aus und meldet das als Hinweis. interface und trait erkennt der Scanner nicht
als Test-Ziel und gibt eine entsprechende Fehlermeldung aus, statt veralteten Output stehenzulassen.
Property Hooks (PHP 8.4) im Klassenrumpf stören den Scanner nicht, werden aber auch nicht eigens
getestet, weil sie sich semantisch eher über den Zustand der Property als über Methodenaufrufe
prüfen lassen. An seine Grenze kommt der Scanner außerdem bei generischen Templates über
PHPDoc oder bei sehr ungewöhnlichen Whitespace-Konstellationen. Für diese Fälle lohnt
sich ein kurzer Blick auf den Output, bevor du ihn ins Projekt übernimmst. Wie der Generator aus
dieser Erkennung dann den fertigen Test baut, regelt ein festes Style-Schema.
So sieht der generierte Code aus
Der erzeugte Test ist PSR-12-konform.
declare(strict_types=1);
steht als erste Anweisung in der Datei, die Klassenklammer landet auf einer eigenen Zeile, ein
abschließendes ?>-Tag wird weggelassen, und jede Testmethode bekommt : void
als Return-Type.
Als Default-Assertion benutzt der Generator durchgehend assertSame(). Der Vorteil
gegenüber assertEquals(): assertSame() vergleicht auch den Datentyp und
nicht nur den Wert. Eine Methode, die statt einer 0 versehentlich den String "0"
zurückgibt, fällt damit sofort auf. Die assertIsInt()/assertIsString()-Familie
meidet der Generator bewusst, weil sie nur den Typ prüft, nicht den konkreten Wert.
Je nach Return-Type setzt der Generator passende Default-Erwartungen:
int, float → assertSame(0 /* TODO */, $result)
string → assertSame('' /* TODO */, $result)
array, iterable → assertSame([] /* TODO */, $result)
bool → assertTrue($result) mit Hinweis auf assertFalse als Alternative
void, never → expectNotToPerformAssertions()
Objekt-Rückgaben oder unbekannte Typen → assertSame(null /* TODO */, $result) als Fallback
Die // TODO: erwartet-Marker zeigen dir, wo du die konkrete Erwartung eintragen musst.
Wirft die Methode laut PHPDoc-@throws-Tag eine Exception, ändert sich der Aufbau:
statt einer Assertion auf den Rückgabewert setzt der Generator ein
$this->expectException(...) vor dem Aufruf, wobei die Klasse aus dem
@throws-Tag übernommen wird. Stehen mehrere @throws über der
Methode, gewinnt der erste Eintrag — expectException() lässt sich in PHPUnit
nicht stapeln, jeder weitere Aufruf würde den vorherigen überschreiben. Die zusätzlichen
Exception-Klassen tauchen als Kommentar im Test auf, damit du sie für eigene Testmethoden
sofort siehst. Die // Act-Zeile ruft die Methode dann ohne $result = auf, weil
eine geworfene Exception die Zuweisung unterbricht und PHPUnit selbst prüft, ob der
Erwartungswert eingetreten ist.
Sind im Konstruktor Abhängigkeiten vom Typ einer Klasse oder eines
Interfaces
definiert und ist die Option Mocks aktiviert, erzeugt der Generator zusätzlich ein
vorgefertigtes setUp() mit
$this->createMock()-Aufrufen
und einer fertig zusammengebauten $sut-Property (System under Test). Damit bist du in jeder
Testmethode sofort startbereit. Für das Testen von private- und
protected-Methoden setzt der Generator auf ReflectionMethod, sobald du die
entsprechende Option aktivierst. So kannst du auch interne Hilfsmethoden testen, ohne sie für den
reinen Testzweck auf public hochzustufen.
Beispiel: Eingabe und Ausgabe gegenübergestellt
Damit klarer wird, was das Tool eigentlich tut, hier ein Vorher-Nachher mit etwas mehr Fleisch am
Knochen. Aus dieser readonly class mit einer Konstruktor-Abhängigkeit, einem
Variadic-Parameter und einem dokumentierten @throws:
<?php
namespace App\Service;
use App\Repository\UserRepository;
final readonly class Notifier
{
public function __construct(private UserRepository $users) {}
/**
* @throws \RuntimeException when sending fails
*/
public function notify(int $userId, string ...$tags): bool
{
return $this->users->mark($userId, $tags);
}
}
generiert der Generator mit aktivierten Mocks folgendes Test-Skelett. Achte auf drei Details: die
readonly-Variante wird sauber erkannt, der Variadic-Parameter erscheint als zwei
Beispiel-Werte im Aufruf, und der @throws-Block aus dem PHPDoc verwandelt sich in einen
eigenen // Expect-Abschnitt mit expectException(). Die
// Act-Zeile läuft folgerichtig ohne $result =:
<?php
declare(strict_types=1);
namespace Tests\Service;
use App\Service\Notifier;
use PHPUnit\Framework\TestCase;
#[\PHPUnit\Framework\Attributes\CoversClass(Notifier::class)]
final class NotifierTest extends TestCase
{
/** @var \PHPUnit\Framework\MockObject\MockObject&UserRepository */
private $users;
private Notifier $sut;
protected function setUp(): void
{
parent::setUp();
$this->users = $this->createMock(UserRepository::class);
$this->sut = new Notifier($this->users);
}
public function testNotify(): void
{
// Arrange
// Expect
$this->expectException(\RuntimeException::class);
// Act
$this->sut->notify(0, '', '' /* variadic */);
}
}
An solchen Stellen merkst du, wieviel Tipparbeit dir der Generator abnimmt. Wer noch tiefer in das
Thema einsteigen will, findet im
PHPUnit-Tutorial
die Erklärung der einzelnen Assertions, im Tutorial zu
PSR-4-Autoloading
die passende Verzeichnisstruktur für dein Test-Setup und im
Composer-Tutorial
eine Anleitung, wie du PHPUnit als Dev-Dependency in dein Projekt aufnimmst.