Antworten auf Fragen, die mir immer wieder zu PHPUnit, Testautomatisierung, Code Coverage und dem Testen in PHP gestellt werden. Wo es passt, verweist eine Antwort auf einen ausführlichen Artikel.
Nutze PCOV, wenn dir Line Coverage genügt: Es ist deutlich schneller und ideal für die CI. Greife zu Xdebug, wenn du Branch- oder Path Coverage brauchst oder Xdebug ohnehin schon zum Debuggen einsetzt. Beide gleichzeitig zu aktivieren, ergibt selten Sinn.
Mehr dazu im Artikel: PCOV oder Xdebug?
Ja. Sammle die Coverage für jeden Lauf getrennt, etwa wenn du deine Suite shardest oder parallelisierst, exportiere sie im PHP- oder Cobertura-Format und führe die Berichte anschließend zusammen. So erhältst du einen Gesamtwert, ohne alle Tests in einen einzigen Lauf zwingen zu müssen.
Mehr dazu im Artikel: Code Coverage-Daten zusammenführen
Line Coverage sagt nur, welcher Code ausgeführt wurde, nicht, ob deine Assertions wirklich etwas prüfen. Path Coverage zeigt ungetestete Kombinationen von Verzweigungen, und Mutation Testing misst, ob deine Tests eine Änderung am Code überhaupt bemerken würden. Beides deckt Vertrauen auf, das Coverage allein nicht belegt.
Mehr dazu im Artikel: Path Coverage oder Mutation Testing?
withConsecutive() wurde in PHPUnit 10 entfernt. Ersetze es durch einen einzelnen willReturnCallback(), der die Argumente prüft, bei Bedarf kombiniert mit dem Invocation-Matcher aus $this->exactly(), sodass du auf jeden Aufruf anhand seiner Nummer reagieren kannst. Das ist ausführlicher, aber deutlich klarer in dem, was jeder Aufruf erwartet.
Mehr dazu im Artikel: Besser als withConsecutive()
Nutze einen Stub, wenn ein Kollaborateur nur vorgegebene Werte liefern soll, damit der getestete Code läuft. Nutze einen Mock, wenn gerade die Interaktion selbst überprüft werden soll. Aufrufe zu verifizieren, die keine Rolle spielen, koppelt deine Tests an die Implementierung und macht sie brüchig.
Mehr dazu im Artikel: Die Stub-/Mock-Intervention
Frag dich, wofür du das Test Double überhaupt brauchst. Ist dir der Aufruf wichtig, dann ersetze any() durch einen konkreten Matcher wie once() oder exactly(2). Ist er dir nicht wichtig, dann brauchst du kein Mock Object, sondern einen mit createStub() erzeugten Test Stub. expects($this->any()) sagt aus, dass du Kommunikation prüfen willst, sie dir aber egal ist, und dieser Widerspruch ist der Grund für die Deprecation.
Mehr dazu im Artikel: Vom Anti-Pattern zur Klarheit
Nein. Ein Wertobjekt steht für einen festen Wert, es gibt also nichts, wovon du den getesteten Code isolieren müsstest. Verwende echte Instanzen, dann testest du gegen dasselbe Verhalten, das auch in Produktion gilt. Für DTOs gilt dasselbe, besonders wenn du sie unveränderlich entwirfst.
Mehr dazu im Artikel: Testen mit DTOs und Wertobjekten
Es gibt vier Ebenen, die du dir ansehen solltest: die Konfiguration des Test-Runners, die Infrastruktur, die deine Tests berühren, die Parallelisierung und das Design der Tests selbst. Die größten und nachhaltigsten Gewinne kommen fast immer aus besserem Test-Design, nicht daraus, einer langsamen Suite mehr Kerne zu geben.
Mehr dazu im Artikel: Turbo-Charging Your PHPUnit Suite
PHPUnit weiß bei jedem Lauf, wie lange jeder Test gedauert hat, wie viel CPU-Zeit er verbraucht und wie viel Speicher er belegt hat. Mit --log-otr schreibt es diese Daten in eine Open-Test-Reporting-Logdatei. Das Werkzeug otr-report liest sie zurück und zeigt dir die langsamsten Tests, auf Wunsch nur die, die über der mittleren Laufzeit liegen. Das ist fast immer ehrlicher als die Intuition.
Mehr dazu im Artikel: Was dein Testlauf längst weiß
Miss, bevor du irgendetwas änderst. Ein Profiler zeigt dir, wohin Zeit und Speicher tatsächlich fließen, und das ist regelmäßig woanders als vermutet. Raten und nach Bauchgefühl zu optimieren, macht den Code meist komplexer, ohne ihn schneller zu machen.
Mehr dazu im Artikel: Debugging von Performance in PHP
Nein. setUp() läuft vor jedem Test der Klasse, auch vor denen, die das dort erzeugte Testinventar gar nicht verwenden. Kommt Vererbung ins Spiel, legt ein vergessener parent::setUp()-Aufruf still, was die Basisklasse bereitstellt. Ich schreibe stattdessen private Methoden in der Testklasse, die das benötigte Objekt erzeugen, und rufe sie nur aus den Tests auf, die es brauchen. Der Test hält danach keine Referenz mehr darauf, und jeder Test macht seine Abhängigkeiten selbst sichtbar.
Mehr dazu im Artikel: Wie ich Testinventar verwalte
Mit Characterization Tests. Sie dokumentieren das tatsächliche Verhalten des Codes, ohne zu beurteilen, ob dieses Verhalten richtig ist: Schreibe einen Test mit einer Assertion, von der du weißt, dass sie scheitern wird, lies das tatsächliche Ergebnis aus der Fehlermeldung ab und übernimm es in die Assertion. Wiederhole das mit verschiedenen Eingaben, bis du genügend Abdeckung hast. Damit hast du das Sicherheitsnetz, das du brauchst, bevor du den Code in kleinere, testbare Einheiten zerlegst.
Mehr dazu im Artikel: Ein Flugschreiber für deinen Code
Beides führt eine Testmethode mit vielen Eingaben aus, beantwortet aber unterschiedliche Fragen. Ein Data Provider hält konkrete Beispiele fest: Bei dieser Eingabe muss genau dieses Ergebnis herauskommen. Eine Property beschreibt eine Aussage, die für alle Eingaben gelten muss, und überlässt die Beispiele einem Generator. Data Provider dokumentieren deinen aktuellen Wissensstand, Properties decken die Lücken darin auf.
Mehr dazu im Artikel: Data Provider oder Properties?
Statt einzelne Beispiele zu prüfen, formulierst du eine Eigenschaft, die für alle gültigen Eingaben gelten soll, und lässt ein Werkzeug viele davon erzeugen, auch Randfälle, an die du nicht denken würdest. Findet es einen Fehler, verkleinert es die Eingabe auf den kleinsten Fall, der noch scheitert.
Mehr dazu im Artikel: Property-Based Testing
Was das entscheidet, heißt Test Oracle. Bei einem Rückgabewert ist das Orakel der Vergleich mit dem erwarteten Wert. Bei einer Zustandsänderung ist es die Prüfung des Zustands nach der Ausführung, und bei der Kommunikation mit kollaborierenden Objekten sind es die Erwartungen, die du auf deinem Mock Object konfiguriert hast. Wenn du nicht benennen kannst, was in einem Test das Orakel ist, prüft dieser Test womöglich gar nichts.
Mehr dazu im Artikel: Die Wahrheit erkennen: Test Oracles
Im Idealfall nicht. Der beste Bugfix fügt einen Regressionstest hinzu und schreibt keine bestehende Erwartung um: Der neue Test belegt, dass der Fehler behoben ist, die unveränderten belegen, dass nichts anderes kaputtgegangen ist. Zwingt dich ein Fix doch dazu, eine bestehende Erwartung umzuschreiben, halte inne. Meist trifft eines von drei Mustern zu: Der Test war an Implementierungsdetails gekoppelt, der Fix ist in Wahrheit eine Verhaltensänderung und sollte auch so kommuniziert werden, oder du reaktivierst einen Test, der früher stillgelegt wurde.
Mehr dazu im Artikel: Unveränderte Tests sind der halbe Beweis
Kapsle ein Array in einer eigenen Klasse, die nur den gewünschten Typ annimmt, biete genau die Operationen an, die du wirklich brauchst, und lass die statische Analyse den Elementtyp über Generics-Annotationen prüfen. Du gewinnst Sicherheit und eine klare API, statt rohe Arrays herumzureichen.
Mehr dazu im Artikel: Typsichere Collections
Ich nutze PHPStan, vor allem wegen des großen Ökosystems an Erweiterungen für Frameworks und Bibliotheken. Wer neu in der statischen Analyse ist, fährt damit gut. Beide Werkzeuge gleichzeitig auf denselben Code loszulassen, empfehle ich nicht: Ihre Regeln können sich widersprechen, und dann versuchst du, zwei unterschiedliche Auffassungen von gutem Code gleichzeitig zufriedenzustellen. Wichtiger als die Wahl des Werkzeugs ist die Entscheidung, überhaupt statische Analyse einzusetzen.
Mehr dazu im Artikel: Psalm oder PHPStan?
Für das Einhalten von Coding Standards nutze ich PHP-CS-Fixer: Alles, was dieses Werkzeug beanstanden kann, kann es auch automatisch korrigieren. Lokal lasse ich es vor dem Commit korrigieren, in der CI läuft es im Check-Modus und meldet Abweichungen nur. PHP_CodeSniffer setze ich weiterhin dort ein, wo ich Code analysiere statt ihn zu formatieren, etwa bei Due Diligence, Security Audits und Guided Refactoring, mit eigenen Sniffs und Regelsätzen wie PHPCompatibility.
Mehr dazu im Artikel: PHP_CodeSniffer oder PHP-CS-Fixer?
Kein einzelnes Verfahren reicht aus. Statische Analyse findet Typfehler und Unstimmigkeiten, ohne den Code auszuführen. Unit Tests prüfen konkretes Verhalten, eigenschaftsbasiertes Testen verallgemeinert es, Mutation Testing misst die Qualität deiner Tests, und Fuzzing erkundet, woran niemand gedacht hat. Jede Schicht deckt einen anderen Ausschnitt der unendlich vielen Arten ab, auf die Code falsch sein kann.
Mehr dazu im Artikel: Alles, was wir haben
Für fast jede Schwachstelle, die es in Produktion schafft, gibt es einen Test, der sie verhindert hätte, hätte es ihn gegeben. Test-Driven Security behandelt bekannte Schwachstellenkategorien als Checkliste und nutzt die Testwerkzeuge, die du ohnehin hast, um die Abwesenheit jeder Fehlerklasse zu belegen.
Mehr dazu im Artikel: Test-Driven Security
Ja. Ist deine Suite zu langsam, um sie oft laufen zu lassen, können auch automatische Werkzeuge und KI-Agenten sie nicht zur Prüfung ihrer Änderungen nutzen, und Schwachstellen bleiben länger verborgen. Was früher rein als Produktivitätsproblem galt, gehört heute zur Sicherheitsdebatte.
Mehr dazu im Artikel: Geschwindigkeit als Sicherheitsmerkmal
Nein. PHPUnit ist eine Entwicklungsabhängigkeit: Fordere es unter require-dev an und stelle sicher, dass es niemals in deinem Produktiv-Deployment oder Autoloader landet. Testwerkzeuge in Produktion auszuliefern, vergrößert die Angriffsfläche ohne Not.
Mehr dazu im Artikel: PHPUnit: Ein Sicherheitsrisiko?
Seit Composer 2.9 erledigt das der Resolver selbst: Versionen mit bekannten Security-Advisories werden standardmäßig gar nicht mehr in Betracht gezogen, und zwar bei jeder Auflösung des Abhängigkeitsgraphen. Vorher war composer audit rein informierend, und wer eine harte Garantie wollte, brauchte ein Paket wie roave/security-advisories. Rechne damit, dass dich das an Stellen trifft, an denen das passive Audit dich nur informiert hat: Eine Installation kann jetzt fehlschlagen, weil eine verwundbare Version nicht mehr auflösbar ist.
Mehr dazu im Artikel: Der Türsteher im Dependency-Resolver
Behandle einen Workflow als Code, der Geheimnisse berührt. Der häufigste und gefährlichste Fehler ist, einen ${{ ... }}-Ausdruck in ein Shell-Skript zu interpolieren: Der Wert wird eingesetzt, bevor die Shell das Skript überhaupt liest, und so wird ein in einem Fork frei gewählter Branch-Name zu ausgeführtem Code. Übergib solche Werte stattdessen über Umgebungsvariablen und quote sie im Skript. Ein statischer Analysator für Workflow-Dateien wie zizmor findet diese und verwandte Schwächen in wenigen Minuten.
Mehr dazu im Artikel: GitHub Actions Workflows härten
Meistens nicht, garantiert ist es aber nicht. In PHPCSUtils gab es eine Schwachstelle (CVE-2026-65954), bei der ein Sniff den Quelltext eines Array-Schlüssels an eval() übergeben hat. Ein präparierter Array-Schlüssel wurde damit ausgeführt, während die Datei analysiert wurde, etwa im CI-Job, der jeden Pull Request prüft. Halte deine Analysewerkzeuge aktuell. disable_functions hilft hier übrigens nicht: eval() ist ein Sprachkonstrukt und hat keinen Eintrag in der Funktionstabelle.
Mehr dazu im Artikel: Wenn statische Analyse deinen Code ausführt
Eine neue Major Version von PHPUnit erscheint jedes Jahr am ersten Freitag im Februar und ist die Gelegenheit für Aufräumarbeiten. Was sie ändert, wird ein Jahr vorher mit dem Release der vorherigen Major Version angekündigt. Wer diese Ankündigung liest und die nötigen Anpassungen über das Jahr verteilt einplant, erlebt keine Überraschung. Lass ein Testwerkzeug nicht unbeaufsichtigt auf eine neue Major Version aktualisieren.
Mehr dazu im Artikel: Hilfe! Meine Tests funktionieren nicht mehr
PHPUnit unterscheidet zwei Stufen. Eine Soft Deprecation führt während der Testausführung noch zu keiner Meldung; IDEs und statische Analysewerkzeuge können aber bereits warnen. Eine Hard Deprecation wird bei jedem Testlauf gemeldet. In beiden Fällen funktionieren deine Tests weiter, und ein hart deprecated Feature wird frühestens in der nächsten Major Version entfernt.
Mehr dazu im Artikel: Vom Anti-Pattern zur Klarheit
Grüne Tests belegen, dass der Code tut, was die Tests prüfen, nicht dass er das Richtige tut. Ich habe einen KI-Agenten eine nicht triviale Softwaremetrik implementieren lassen. Er brauchte 15 Minuten, ich anschließend Stunden, um zu beurteilen, ob die Umsetzung korrekt ist. Code zu schreiben war nie der Engpass: Das Problem zu verstehen, die Lösung zu entwerfen und das Ergebnis zu überprüfen sind die schwierigen Teile.
Mehr dazu im Artikel: Schneller als Verstehen
Dasselbe, was auch menschlichen Teams hilft, nur verbindlicher: eine Testsuite, der du vertraust, statische Analyse, gepflegte Dokumentation und Code Reviews. Ein Agent kann die Codebasis analysieren, eine Implementierung samt Tests schreiben, diese ausführen und einen Pull Request vorbereiten, ohne dass jemand zusieht. Diese Praktiken sind der Rahmen, der solche Autonomie überprüfbar macht.
Mehr dazu im Artikel: Mehr als Best Practices
Immer dann, wenn du das Problem noch nicht verstanden hast. Jede Zeile, die du nicht schreibst, muss niemand debuggen, warten, testen oder in fünf Jahren wieder verstehen. Das gilt besonders für Code, den ein KI-Agent in Minuten erzeugt: Wenn du seine Korrektheit nicht beurteilen kannst, wächst dein Wartungsaufwand, ohne dass du weißt, wofür.
Mehr dazu im Artikel: Code, den ich nicht habe, macht keine Probleme
Ja. Ich biete maßgeschneiderte Workshops und Schulungen zu PHPUnit, Testautomatisierung und testbarem Software-Design, remote oder vor Ort. Die aktuellen Themen findest du im Schulungsbereich.
Das ist ein großer Teil meiner Arbeit. Durch Beratung und Coaching helfe ich Teams, Legacy-Code unter Test zu bringen und die Gewohnheiten aufzubauen, die ihn dort halten. Der Beratungsbereich beschreibt, wie das abläuft.