Wenn es ums Testen geht, landet das Gespräch sofort bei der Pyramide: unten viele Unit-Tests, in der Mitte ein paar Integrationstests, oben ein paar End-to-End-Tests. Ein hübsches Diagramm. So arbeite ich nicht.
Hier steht, was ich wirklich mache, inklusive der Teile, für die man mich in einem Review meiner Teststrategie vermutlich rüffeln würde.
Ich schreibe den Funktionstest zuerst, nicht den Unit-Test
Wenn ich einen Endpunkt baue, ist das Erste, was ich schreibe, ein Test, der ihn über HTTP aufruft und die Antwort prüft. Bevor der Controller existiert. Bevor der Service existiert.
public function testCreateApplicationReturnsCreated(): void
{
$client = static::createClient();
$client->request('POST', '/api/applications', [], [], [
'CONTENT_TYPE' => 'application/json',
'HTTP_AUTHORIZATION' => 'Bearer ' . $this->token(),
], json_encode(['title' => 'Backend Developer']));
$this->assertResponseStatusCodeSame(201);
$this->assertJson($client->getResponse()->getContent());
}
Der Grund ist nicht ideologisch. Es ist, dass dieser Test drei Aufgaben gleichzeitig erledigt, und jede davon würde mich sonst ein eigenes Werkzeug oder einen eigenen Nachmittag kosten:
- er ist die Spezifikation — die Form des Endpunkts steht fest, bevor ich ihn schreibe
- er ist der REST-Client — ich mache nie Postman auf, um zu sehen, ob die Route funktioniert
- er ist das Regressionsnetz — er bleibt danach in der Suite

Warum das während der Entwicklung einen REST-Client schlägt
Die übliche Schleife sieht so aus: Code schreiben, zu Postman wechseln, Request abschicken, Antwort lesen, zurückwechseln, etwas ändern, wieder abschicken. Es funktioniert. Es produziert auch nichts, was den Nachmittag überlebt.
Die Funktionstest-Schleife hat gleich viele Schritte, nur liegt der Request im Repository, er trägt den Auth-Header und den JSON-Body schon, und ihn erneut auszuführen ist ein Tastendruck. Wenn das Feature fertig ist, musst du keine Tests mehr schreiben — du hast sie schon.
Wenn du den Request sowieso hundertmal schickst, kannst du ihn genauso gut einmal in eine Datei schreiben, die bleibt.
Ich sage nicht, wirf die REST-Clients weg. Sie sind das richtige Werkzeug, um eine API zu erkunden, die du nicht gebaut hast — dazu habe ich einen ganzen Artikel geschrieben. Aber für eine API, die du gerade baust, ist der Funktionstest schlicht besser, weil er die einzige Version dieses Requests ist, die es nächsten Monat noch gibt.
Wo Unit-Tests ins Spiel kommen
Sobald der Endpunkt funktioniert, sagt mir der Funktionstest, dass der Happy Path in Ordnung ist. Für alles rundherum ist er ein schlechtes Werkzeug, weil jeder Fall einen kompletten HTTP-Roundtrip und eine Datenbank braucht.
Die Unit-Tests kommen also als Zweites, und sie gehen dorthin, wo das eigentliche Denken steckt: die Berechnung, der Zustandsautomat, der Parser, das Ding mit vierzehn Sonderfällen. Die werden direkt getestet, ohne Framework, ohne Container, ohne Datenbank — nur eine Klasse und eine Tabelle aus Eingaben und erwarteten Ausgaben.
Diese Aufteilung hat sich bei mir gut gehalten. Funktionstests beantworten "ist es richtig verdrahtet", Unit-Tests beantworten "stimmt die Logik". Zu versuchen, dass eines von beiden beide Fragen beantwortet, ist der Punkt, an dem Testsuiten langsam und brüchig werden.
Was ich bewusst nicht teste
Getter und Setter. Framework-Verhalten. Alles, dessen Test ein Spiegelbild der Implementierung wäre — wenn eine Codeänderung immer bedeutet, den Test in genau derselben Form zu ändern, prüft der Test, dass du etwas zweimal getippt hast.
Fremde APIs auch. Ich habe keinen Test, der die echte LinkedIn-API aufruft. Ich habe einen Test, der prüft, dass mein Code den richtigen Request schickt und die vier Antworten behandelt, die mich interessieren, mit gemocktem HTTP-Client. Ob LinkedIn erreichbar ist, kann meine Testsuite um 2 Uhr nachts nicht reparieren.
Coverage ist eine Diagnose, kein Ziel
Ich schaue mir Coverage an, um die Dateien zu finden, die nie jemand getestet hat — das ist wirklich nützlich. Ich setze keine Prozentzahl, die der Build erreichen muss, denn der schnellste Weg, Coverage zu erhöhen, ist, Tests für den Code zu schreiben, der ohnehin offensichtlich korrekt war, und jetzt hast du eine größere Suite, die genauso viele Bugs findet wie vorher.
Die Frage, die sich bei einer Testsuite lohnt, ist nicht "wie viel Prozent deckt sie ab". Sie ist: "Wenn sie grün wird, glaube ich dann tatsächlich, dass die Software funktioniert?" Wenn die Antwort nein ist, repariert mehr Coverage das nicht.
Die Tests müssen trivial ausführbar sein
Das alles bricht zusammen, wenn die Suite laufen zu lassen nervig ist. Wenn ein Testlauf eine bestimmte PHP-Version auf dem Laptop braucht, eine manuell befüllte Datenbank oder ein fünfstufiges Setup aus der README, dann führen manche Leute Tests aus und manche nicht — und die, die es nicht tun, machen Dinge kaputt, ohne es je zu sehen.
Also läuft die Suite im Container, für alle gleich, und sie läuft aus der IDE heraus per Rechtsklick. Das kostet etwas Konfiguration, die ich in wie du PHPUnit lokal in PhpStorm mit Docker ausführst aufgeschrieben habe.
Das Setup ist der langweilige Teil. Es ist auch der Teil, der entscheidet, ob irgendetwas vom Rest überhaupt passiert.