Häufige Fragen
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.
Code Coverage
Sollte ich PCOV oder Xdebug für Code Coverage verwenden?
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?
Kann ich Code-Coverage-Daten aus mehreren Testläufen zusammenführen?
Ja. Lass jeden Lauf seine Daten mit --coverage-php in eine eigene .cov-Datei schreiben, etwa wenn du deine Suite shardest oder parallelisierst, und führe diese Dateien anschließend mit dem merge-Befehl von PHPCOV zu einem Bericht zusammen. Seit PHPUnit 13.1 und PHPCOV 13.0 enthalten die Dateien relative Pfade. Deshalb funktioniert das auch, wenn die Läufe auf verschiedenen Maschinen stattfanden oder teils mit dem PHAR und teils mit einem über Composer installierten PHPUnit. Mit --include-git-information prüft PHPCOV zusätzlich, dass alle Dateien zum selben Stand des Codes gehören.
Mehr dazu im Artikel: Code Coverage-Daten zusammenführen
Meine Coverage ist grün, trotzdem rutschen Fehler durch. Was nun?
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 zeigt, ob das Vertrauen, das Coverage dir gibt, gerechtfertigt ist.
Mehr dazu im Artikel: Path Coverage oder Mutation Testing?
Sollten wir eine Mindest-Coverage vorschreiben?
Nicht als Ziel, das von außen vorgegeben wird. Sobald ein Team 80, 90 oder gar 100 % erreichen muss, weil eine Vorgabe, ein Dashboard oder ein Bonus daran hängt, wird die Metrik optimiert und nicht mehr das, was sie messen sollte. Dann werden die trivialen Teile getestet, weil sie leicht abzudecken sind, und Tests rufen Methoden auf, ohne deren Ergebnis zu prüfen. PHPUnit fängt die gröbste Form davon ab: Ein Test ohne Assertion gilt als riskant, und seine Coverage-Daten werden verworfen. Schwache Assertions deckt erst Mutation Testing auf. Die Entscheidung, eine Metrik als Indikator zu behandeln und nicht als Ziel, nimmt dir aber kein Werkzeug ab.
Mehr dazu im Artikel: Wenn die Metrik zum Ziel wird
Wie sehe ich, welchen Code meine Unit Tests allein abdecken?
Deklariere die Größe deiner Tests mit den Attributen #[Small], #[Medium] und #[Large]. Seit PHPUnit 13.3 lässt sich der HTML-Bericht für Code Coverage nach diesen Größen filtern, und alle Zahlen auf der Seite werden neu berechnet. Eine Klasse kann insgesamt zu 100 % abgedeckt sein, obwohl nur ein Bruchteil davon aus kleinen Tests stammt und der Rest nur über große End-to-End-Tests erreicht wird. Ein Klick auf Small zeigt dir, was deine Unit Tests allein abdecken. Der Unterschied zwischen All und der Kombination der drei Größen zeigt dir außerdem, wie viel deiner Coverage aus Tests stammt, die gar keine Größe deklarieren.
Mehr dazu im Artikel: Wiederholung und Durchblick: Was ist neu in PHPUnit 13.3?
Test Doubles (Mocks und Stubs)
Was nutze ich statt withConsecutive()?
withConsecutive() wurde in PHPUnit 10 entfernt. Seit PHPUnit 13 gibt es zwei Regeln, die es ersetzen: withParameterSetsInOrder() erwartet die Parametersätze in genau der angegebenen Reihenfolge, withParameterSetsInAnyOrder() erwartet nur, dass alle angegebenen Parametersätze verwendet werden. PHPUnit 13.2 ergänzt withParameterSetsInPartialOrder() für den Fall dazwischen, in dem du einige Parametersätze auf feste Positionen festlegst und die übrigen ungeordnet lässt. Verlange die strenge Reihenfolge nur, wenn sie für die Korrektheit wirklich zählt. Sonst schlägt dein Test fehl, sobald jemand die Aufrufe umsortiert, ohne am beobachtbaren Verhalten etwas zu ändern. Wer noch PHPUnit 10, 11 oder 12 verwendet, muss weiterhin zu einem willReturnCallback() greifen, der die Argumente selbst prüft.
Mehr dazu im Artikel: Besser als withConsecutive()
Wann nutze ich einen Stub und wann einen Mock?
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
PHPUnit meldet, dass für mein Mock Object keine Erwartungen konfiguriert wurden. Was nun?
Diese Meldung gibt es seit PHPUnit 12.5, wenn ein Test mit createMock() ein Mock Object erzeugt, aber keine Erwartung darauf konfiguriert. Entweder hast du die Erwartungen vergessen und solltest sie ergänzen, oder du brauchst gar kein Mock Object, sondern einen Test Stub, den du mit createStub() erzeugst. Die Unterscheidung ist Absicht: createStub() sagt allen, die den Test lesen, dass eine Abhängigkeit entkoppelt wird, createMock() sagt ihnen, dass die Kommunikation zwischen Objekten geprüft wird. Seit PHPUnit 12 lassen sich auf einem Test Stub auch technisch keine Erwartungen mehr konfigurieren.
Mehr dazu im Artikel: Testen mit (und ohne) Abhängigkeiten
Der any()-Matcher ist deprecated. Wie migriere ich?
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
Soll ich Wertobjekte und DTOs durch Test Doubles ersetzen?
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
Performance und Debugging
Wie wird meine PHPUnit-Testsuite schneller?
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
Wie finde ich heraus, welche Tests meine Suite ausbremsen?
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ß
Wie finde ich Performance-Probleme in PHP?
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
Wie mache ich meine Software leichter debugbar?
Indem du sie auf jeder Ebene explizit machst. Komponenten mit klaren Schnittstellen lassen dich eingrenzen, wo ein Fehler steckt; strukturiertes Logging, Metriken und Tracing zeigen, was im Inneren passiert; deterministische Builds und versionierte Konfiguration lassen dich den Zustand wiederherstellen, in dem der Fehler auftrat. Im Design helfen wenig versteckter Zustand und Fail Fast: Eine Exception an der Stelle, an der ungültige Daten ankommen, ist leichter nachzuvollziehen als ein Fehler drei Aufrufe später. Im Code erledigen aussagekräftige Namen, kleine Methoden, Guard Clauses und Fehlermeldungen, die die beteiligten Werte nennen, den Rest. Mit Event Sourcing hast du zusätzlich die vollständige Geschichte, wie das System in seinen Zustand gekommen ist, und kannst sie bis zu dem Event abspielen, das das Problem ausgelöst hat.
Mehr dazu im Artikel: Debugging als Designziel
Sollte ich OPcache für meine Testsuite einschalten?
Ja. Auf der Kommandozeile ist OPcache standardmäßig aus, denn opcache.enable_cli steht auf 0. Deine Testsuite läuft deshalb auf unoptimiertem Bytecode, bis du das änderst. Trotz seines Namens optimiert OPcache nämlich auch: Fünfzehn Durchläufe bauen den Bytecode zwischen Compiler und Executor um, und ohne OPcache findet keiner davon statt. Bei der Testsuite, an der ich das gemessen habe, kostete das etwa 14 %, 9,91 statt 8,48 Sekunden für einen Lauf. Der Optimierer selbst brauchte dabei etwa 0,15 Sekunden pro Aufruf.
Mehr dazu im Artikel: Wie PHP Bytecode optimiert
Lohnt sich der JIT-Compiler für meine Testsuite?
Das hängt davon ab, wohin die Zeit geht und welchen Modus du wählst. Geht sie in die Datenbank, das Dateisystem oder das Netzwerk, bringt der JIT nichts. Rechnet dein PHP-Code selbst viel, kann er sich lohnen: Bei der Testsuite eines Raytracers mit 177 Tests sank die Laufzeit mit opcache.jit=tracing von 9,16 auf 6,48 Sekunden. Mit opcache.jit=function dauerte derselbe Lauf dagegen 5,8 % länger als ganz ohne JIT, weil der Function JIT alles kompiliert, bevor etwas läuft, auch Code, der nie ausgeführt wird. In keiner Standardinstallation ist der JIT eingeschaltet. Du musst opcache.jit also selbst setzen und OPcache für die Kommandozeile aktivieren. Xdebug und der JIT schließen sich aus: Sobald Xdebug sich in die Ausführung einklinkt, schaltet sich der JIT ab.
Mehr dazu im Artikel: Wie PHP Maschinencode erzeugt
Warum ist dieselbe PHP-Version auf zwei Rechnern unterschiedlich schnell?
Unter anderem, weil der Compiler, mit dem PHP gebaut wurde, über das Dispatch-Modell des Executors entscheidet: wie er von einer Instruktion zur nächsten kommt. Bis PHP 8.5 war ein mit Clang gebautes PHP je nach Benchmark zwischen 2,8 % und 44 % langsamer als ein mit GCC gebautes, bei gleicher Versionsnummer und gleichem PHP-Code. Das mit PHP 8.5 eingeführte Modell TAILCALL hat diesen Abstand für Clang 19 und neuer auf x86-64 und ARM64 geschlossen. Welches Modell in deinem Binary steckt, zeigt dir seit PHP 8.5 die Konstante ZEND_VM_KIND. Wenn du Zahlen zwischen zwei Installationen vergleichst, gehört sie neben die Versionsnummer, ebenso wie opcache.optimization_level und opcache.jit.
Mehr dazu im Artikel: Wie PHP Bytecode ausführt
Test-Design und Teststruktur
Brauche ich setUp(), um das Testinventar vorzubereiten?
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
Mein Projekt hat keine Tests und ein Upgrade steht an. Wo fange ich an?
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
Kann ich die Daten für einen Data Provider direkt an die Testmethode schreiben?
Seit PHPUnit 13.1 geht das mit dem Attribut #[DataProviderClosure]. Statt eine separate Methode über ihren Namen als String zu referenzieren, definierst du den Data Provider als Closure direkt an der Testmethode. Die Closure muss statisch sein und wie eine herkömmliche Data-Provider-Methode ein Array oder ein Traversable mit den Argumentsätzen zurückgeben. Das setzt PHP 8.5 voraus, denn erst seitdem sind Closures in konstanten Ausdrücken erlaubt.
Mehr dazu im Artikel: Closures, Klarheit und Kontrolle: Was ist neu in PHPUnit 13.1?
Data Provider oder eigenschaftsbasiertes Testen?
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?
Was ist Property-based Testing?
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 bringt Shrinking beim Property-based Testing?
Es macht aus einer zufälligen Eingabe, für die eine Eigenschaft nicht gilt, das kleinste Gegenbeispiel, für das sie immer noch nicht gilt. Ohne Shrinking meldet dir der Test vielleicht, dass dein Code für [-12, 5, -7, 3] fehlschlägt. Mit Shrinking wird daraus [-1], und sofort ist klar, dass schon die Annahme falsch war, eine Summe sei nie negativ. Zu jedem Generator gehört dafür ein Shrinker, der weiß, wie sich ein Wert verkleinern lässt. Eris bringt für die gängigen Typen sinnvolle Voreinstellungen mit. Du musst also nichts konfigurieren.
Mehr dazu im Artikel: Kleinere Eingabe, größere Erkenntnis
Woher weiß ein Test, was richtig ist?
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
Wie teste ich ein System mit Event Sourcing?
Du prüfst nicht mehr den Zustand nach einer Operation, sondern welche Events sie ausgelöst hat. Das Muster Given, When, Then passt dazu genau: Given sind die Events, die bereits passiert sind, When ist das Command, das du ausführst, und Then sind die Events, die du erwartest, oder die Exception, die geworfen werden soll. Die Domain Events aus dem Event Storming werden so zur Vorlage für deine Tests. Eine projektspezifische Basisklasse für Testfälle kann den Event Store verwalten und aus denselben drei Phasen sogar Dokumentation in Event-Storming-Notation erzeugen.
Mehr dazu im Artikel: Von Events zu Erkenntnissen
Wie teste ich eine neue Version meines Systems mit Event Sourcing gegen echte Daten?
Mit Replay Testing: Du spielst die gesamte Historie aus der Produktion in einer Testumgebung ab, aber mit dem neuen Code. Läuft das ohne Fehler durch, weißt du, dass der neue Code mit jedem Randfall der letzten Jahre zurechtkommt. Logikfehler findest du so allerdings nicht. Dafür vergleichst du zusätzlich das Ergebnis mit dem Zustand aus der Produktion, etwa mit einem Snapshot eines Aggregates oder einem Read Model. Alles, was die Systemgrenze verlässt, muss dabei durch Dummies ersetzt werden, und Reaktionen auf Events dürfen keine neuen Commands auslösen, sonst erzeugst du Events, die es nie gegeben hat. Uhrzeit, Zufallswerte und Antworten externer APIs musst du deterministisch machen.
Mehr dazu im Artikel: Replay Testing
Muss ich bestehende Tests anpassen, wenn ich einen Fehler behebe?
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
Wie baue ich typsichere Collections in PHP?
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
Testausführung
Wie spüre ich Flaky Tests auf?
Indem du denselben Test viele Male hintereinander ausführst. Seit PHPUnit 13.3 wiederholt --repeat 100 jeden einzelnen Test bis zu hundertmal und überspringt die restlichen Wiederholungen, sobald eine fehlschlägt. Für einzelne Testmethoden gibt es das Attribut #[Repeat]. Lässt sich ein Flaky Test nicht reparieren, etwa weil er von Netzwerkdiensten abhängt, kannst du ihn mit #[Retry(3)] bis zu dreimal ausführen lassen. Jeder Test, der erst nach einem Fehlversuch bestanden hat, wird in der Zusammenfassung des Testlaufs aufgeführt, und das lässt sich nicht abschalten. Wiederholt werden nur Testmethoden, die void als Rückgabetyp deklarieren und nicht von anderen Tests abhängen.
Mehr dazu im Artikel: Wiederholung und Durchblick: Was ist neu in PHPUnit 13.3?
Wie führe ich genau einen bestimmten Test aus?
Seit PHPUnit 13.2 über Test-IDs. Eine Test-ID benennt einen einzelnen Test eindeutig, bei einem Test mit Data Provider einschließlich des Datensatzes. --list-test-ids gibt alle Tests deiner Suite als Test-IDs aus, und --run-test-id führt genau den Test aus, auf den die übergebene ID verweist. Anders als bei --filter wird dabei exakt verglichen und kein regulärer Ausdruck verwendet, der versehentlich mehr Tests oder gar keinen trifft. Mit --test-id-filter-file übergibst du eine Datei mit einer Test-ID pro Zeile, etwa um nur die zuletzt fehlgeschlagenen Tests erneut auszuführen oder eine große Suite auf mehrere Worker aufzuteilen.
Mehr dazu im Artikel: Präzision und Klarheit: Was ist neu in PHPUnit 13.2?
Statische Analyse, Coding Standards und Code Reviews
Psalm oder PHPStan?
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?
PHP_CodeSniffer oder PHP-CS-Fixer?
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?
Reichen Tests, oder brauche ich zusätzlich statische Analyse?
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
Machen Typen Tests überflüssig?
Nein. Typen ersetzen keine Tests, aber sie machen den Code starr, und was ein Typ zusichert, musst du nicht testen. Wo der Rückgabetyp die Zusage macht, braucht es weder einen Cast noch einen Test dafür. Solche Stellen zeigt dir Mutation Testing: Ein überlebender Mutant kann bedeuten, dass ein Test zu oberflächlich ist, oder dass der Code, den er verändert, gar nichts tut, etwa ein (int)-Cast auf den Rückgabewert von count(). Seit Infection 0.30 läuft PHPStan mit --static-analysis-tool=phpstan über die Mutanten, die den Tests entkommen sind, und tötet diejenigen, die gegen die deklarierten Typen verstoßen.
Mehr dazu im Artikel: Wo Tests aufhören und Typen anfangen
Welche Strategie für Code Reviews passt zu meinem Team?
Das hängt von zwei Fragen ab: Findet das Review statt, während der Code entsteht, oder danach, und wartet die Änderung darauf? Pair Programming und Ensemble Programming prüfen synchron und vor dem Merge; das lohnt sich bei kritischen Änderungen, riskanten Refactorings und beim Onboarding. Pull Requests prüfen asynchron, blockieren den Merge aber trotzdem. Das passt zu verteilten Teams, kostet aber Wartezeit und führt in schriftlichen Kommentaren leichter zu Missverständnissen. Ein Review nach dem Push bringt das meiste Tempo, funktioniert aber nur mit kleinen Commits, Feature Flags oder Branch by Abstraction, umfassenden automatisierten Tests und Änderungen, die sich schnell zurücknehmen lassen. Egal, wofür du dich entscheidest: Prüfe Architektur, Designentscheidungen und Verständlichkeit, nicht die Formatierung.
Mehr dazu im Artikel: Effektive Code Reviews
Sicherheit
Was ist Test-Driven Security?
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
Kann eine langsame Testsuite ein Sicherheitsrisiko sein?
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.
Mehr dazu im Artikel: Geschwindigkeit als Sicherheitsmerkmal
Ist es sicher, PHPUnit in Produktion zu installieren?
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?
Führt statische Analyse wirklich niemals meinen Code aus?
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
Brauche ich Fuzzing, wenn meine Tests den Code bereits vollständig abdecken?
Für Code, der Eingaben aus nicht vertrauenswürdigen Quellen verarbeitet, ja. Meine Bibliothek csv-parser hatte 100 % Line und Branch Coverage, über 70 % Path Coverage und einen Covered Code Mutation Score von 100 %. Trotzdem fand PHP-Fuzzer einen Fehler: Numerische Strings, die größer sind, als ein 64-Bit-Integer fassen kann, wurden nicht korrekt verarbeitet. Property-based Testing braucht ausdrücklich formulierte Eigenschaften und läuft so schnell wie Unit Tests. Ein Coverage-gesteuerter Fuzzer kommt mit dem impliziten Orakel „stürzt nicht ab“ aus, muss dafür aber Stunden oder Tage laufen.
Mehr dazu im Artikel: Sicherheit durch Chaos
Jemand hat eine Sicherheitslücke in meinem Paket gemeldet. Was nun?
Halte die Details geheim, bis es eine korrigierte Version gibt, und antworte der meldenden Person schnell, auch wenn es nur kurz ist. Behandle die Meldung als Behauptung und prüfe sie: welche Schwachstelle sie beschreibt, wer sie auslösen kann, was sich damit erreichen lässt und welche Versionen betroffen sind. Führe Proof-of-Concept-Code nie auf deinem Entwicklungsrechner aus, auf dem deine SSH-Schlüssel und Zugangsdaten liegen, sondern in einer Sandbox, einem Container oder einer Wegwerf-VM, und mach aus dem Reproducer einen fehlschlagenden Test. Bereite die Korrektur in einem temporären privaten Fork vor, veröffentliche zuerst das Release und kurz danach das GitHub Security Advisory, und gib die betroffenen Versionen als präzise Composer-Constraints an: Seit Composer 2.9 macht ein zu weiter Bereich auch nicht betroffene Versionen uninstallierbar. Das Ecosystem Security Team der PHP Foundation hilft bei der Einschätzung und bei der koordinierten Veröffentlichung.
Supply Chain Security
Wie verhindere ich, dass eine Abhängigkeit mit bekannter Schwachstelle installiert wird?
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
Schützt mich Composer auch vor kompromittierten Paketen?
Seit Composer 2.10 zu einem guten Teil, und zwar ohne Konfiguration. Versionen, die als Malware markiert sind, blockiert Composer nicht nur bei composer update, sondern auch bei composer install. Eine bösartige Version, die erst nach dem Erzeugen deiner composer.lock markiert wurde, scheitert also bei der nächsten Installation, statt unbemerkt in die CI oder in die Produktion zu gelangen. Auf Packagist lässt sich die Git-Referenz einer stabilen Version seit Juli 2026 nicht mehr nachträglich ändern. Genau das hatten fast alle jüngeren Angriffe auf Packagist ausgenutzt: Mit gestohlenem Zugang zu GitHub wurden vertrauenswürdige Tags auf bösartige Commits umgesetzt.
Mehr dazu im Artikel: Composer und Packagist im Supply Chain-Stresstest
Ich habe ein fehlerhaftes Release getaggt. Kann ich den Tag löschen und neu setzen?
Nein, und das war auch nie sicher. Automatisierte Systeme reagieren innerhalb von Sekunden auf Änderungen bei Packagist, Mirrors speichern das Archiv unter der ursprünglichen Referenz, und CI-Systeme mit automatischen Updates von Abhängigkeiten haben die Version vielleicht schon in eine Lock-Datei übernommen. Seit dem 7. Juli 2026 blockiert Packagist das Umsetzen des Tags einer stabilen Version, zeigt eine Warnung auf der Paketseite an und benachrichtigt alle Maintainerinnen und Maintainer per E-Mail. Versionsnummern kosten nichts: Die Korrektur gehört in die nächste Patch-Version, notfalls mit einer vierten Stelle, aus 3.2.0 wird dann 3.2.0.1. Und wenn du Pakete auf Packagist pflegst und die Multi-Faktor-Authentifizierung noch nicht eingeschaltet hast, hole das jetzt nach.
Mehr dazu im Artikel: Composer und Packagist im Supply Chain-Stresstest
Wie erzeuge ich eine verlässliche SBOM für mein PHP-Projekt?
Aus dem, was du deklariert hast, und nicht aus dem, was ein Scanner findet. Wohin das Scannen führt, zeigt die Open-Source-Attribution des MG4: Dort steht ein Android-Botnetz, mit großer Wahrscheinlichkeit der ungeprüfte Treffer eines Code-Scanners. Wenn du deine Abhängigkeiten ausschließlich mit Composer verwaltest, steht in deiner composer.lock bereits jede Komponente mit exakter Version, abgesichert durch Hashes. Das Composer-Plugin cyclonedx-php-composer erzeugt daraus eine SBOM im CycloneDX-Format mit SPDX-Lizenzbezeichnern und Package URLs. Kopiere keine Bibliothek an Composer vorbei in dein Repository: Für so eine Abhängigkeit gelten weder das Blockieren bekannter Schwachstellen noch die Malware-Policy, und composer audit meldet sie nicht. Das PHAR von PHPUnit legt seinen Inhalt übrigens selbst offen, mit --manifest, --sbom und --composer-lock.
Mehr dazu im Artikel: Dein composer.lock weiß, was ein Autohersteller nur vermutet
Wie erfahre ich, ob ein bereits ausgeliefertes Release von einer neuen Schwachstelle betroffen ist?
Nur, wenn du danach suchst. composer install aus einer Lock-Datei blockiert keine Versionen mit Security Advisories, und das ist Absicht: Eine Lock-Datei soll dafür sorgen, dass zweimal dasselbe installiert wird. Ein Release, das du 2024 ausgeliefert hast, behält seine verwundbaren Abhängigkeiten deshalb, ohne dass dich in deiner täglichen Arbeit irgendetwas darauf hinweist. Bewahre composer.json und composer.lock jedes Releases auf, das du noch unterstützt, und lass composer audit --locked --no-dev regelmäßig dagegen laufen, etwa per Cronjob oder als geplanten CI-Job. Derselbe Befehl taugt als Release-Gate in deiner Pipeline, denn ein Advisory, das du nicht behandelt hast, führt zu einem Exit-Code ungleich null.
Mehr dazu im Artikel: Was ISO/IEC 18974 von deinem composer.lock will
Wie härte ich meine GitHub Actions-Workflows?
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
Warum sollte ich ungenutzte Branches löschen?
Weil jeder Branch, der Pull Requests annimmt und an eine Pipeline angeschlossen ist, ein Ziel für Poisoned Pipeline Execution ist. Dabei bringt ein harmlos aussehender Pull Request eine veränderte CI-Konfiguration, ein manipuliertes Build-Skript oder eine Datei mit, die die Pipeline verarbeitet. Für PHPUnit zeigte das Security Advisory GHSA-qrr6-mg7r-m243 im April 2026, wie eine in einem Pull Request veränderte phpunit.xml INI-Direktiven wie auto_prepend_file in Kindprozesse einschleusen kann. Schutzregeln für Branches, kurzlebige Runner und zeitlich begrenzte Zugangsdaten bleiben nötig. Ein Branch, den es nicht gibt, kann aber gar keinen Pull Request mit einem solchen Angriff empfangen.
Mehr dazu im Artikel: Angriffsfläche beginnt im Repository
Versionen und Migration
Meine Tests funktionieren nach einem Update von PHPUnit nicht mehr. Was nun?
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
Was bedeutet es, wenn ein Feature in PHPUnit deprecated ist?
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
expectExceptionMessage() ist deprecated. Was nehme ich stattdessen?
Die Methode war irreführend benannt: Sie hat nie verlangt, dass die Meldung der Exception gleich dem erwarteten String ist, sondern nur, dass sie ihn enthält. Seit PHPUnit 13.2 gibt es deshalb eine explizite API. expectExceptionMessageIs() verlangt eine exakte Übereinstimmung, expectExceptionMessageIsOrContains() behält das bisherige Verhalten mit dem Teilstring bei, und expectExceptionMessageMatches() steht weiterhin für reguläre Ausdrücke zur Verfügung. Wenn du migrieren willst, ohne das Verhalten deiner Tests zu ändern, ist expectExceptionMessageIsOrContains() der direkte Ersatz.
Mehr dazu im Artikel: Präzision und Klarheit: Was ist neu in PHPUnit 13.2?
KI in der Softwareentwicklung
Die Tests des KI-Agenten sind grün. Kann ich den Code übernehmen?
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
Wie gewinne ich Vertrauen in Code, den ein KI-Agent geschrieben hat?
Nicht aus den Signalen, die der Agent selbst erzeugt hat. Wenn derselbe Agent Implementierung und Tests schreibt, zeigen grüne Tests nur, dass die Implementierung mit sich selbst übereinstimmt. Ich nenne das geliehenes Vertrauen. Verdientes Vertrauen braucht unabhängige Belege. Für eine KI-generierte Implementierung der Softwaremetrik ACPATH nutze ich drei solche Quellen. Ein naiver Pfadzähler, der offensichtlich korrekt ist, dient als Orakel, gegen das ich die Implementierung mit Tausenden generierter Funktionen vergleiche. Metamorphe Relationen leite ich aus der wissenschaftlichen Arbeit ab und nicht aus dem Code. Und Mutation Testing zeigt, ob die generierten Tests Fehler, die ich absichtlich einbaue, überhaupt bemerken. Das ist langsamer, als die Ausgabe des Agenten zu übernehmen, aber viel schneller, als erst selbst zum Domänenexperten zu werden.
Mehr dazu im Artikel: Vertrauen ist nicht übertragbar
Was braucht mein Projekt, damit KI-Agenten sinnvoll daran arbeiten können?
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
Wann schreibe ich besser gar keinen Code?
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
Wie sollte die Ausgabe meines Werkzeugs aussehen, wenn Coding-Agenten sie lesen?
Dein Werkzeug wird jetzt auf zwei Arten gelesen: von einem Menschen, der ein gerendertes Terminal sieht, und von einem Coding-Agenten, der die rohen Bytes liest. Jeder Unterschied zwischen beiden lässt sich ausnutzen. Zeichenketten aus der Eingabe sollten nicht unverändert in der Ausgabe landen: PHPUnit ersetzt Steuerzeichen und bidirektionale Zeichen durch sichtbare \u{NNNN}-Escapes, damit Escape-Sequenzen einen Fehlschlag nicht wie einen Erfolg aussehen lassen können. Die Eingabe darf auch die Struktur der Ausgabe nicht fälschen können, und ein Aufruf darf nicht hängen bleiben: PHPUnit 13.4 hat --timeout eingeführt, das einen Lauf sauber beendet, die Logs schreibt und mit dem Exit-Code 124 endet. Vor allem brauchst du ein Ergebnis, für das niemand Prosa parsen muss, etwa den Exit-Code und ein maschinenlesbares Log.
PHP und Open Source
Wie melde ich einen Fehler in PHPUnit am besten?
Mit einem Test, der fehlschlägt. Er sagt eindeutig, welcher Aufruf mit welchen Argumenten welches falsche Ergebnis liefert. Ich kann ihn in Sekunden ausführen und muss keinen Diff verstehen. Er fügt eine Erwartung hinzu und ändert keine bestehende, und sobald der Fehler behoben ist, wird er zum Regressionstest. Ein Pull Request, der nichts als einen fehlschlagenden Test enthält, ist mir willkommen. Die Richtlinien für Beiträge zu PHPUnit verlangen außerdem, dass ein Mensch für jeden Beitrag einsteht und offenlegt, wie ein Coding-Assistent verwendet wurde, falls einer im Spiel war.
Mehr dazu im Artikel: Wenn der Diff nichts kostet
Warum kann ich nicht zwei Versionen derselben Bibliothek gleichzeitig verwenden?
Weil die PHP-Engine jeden Klassennamen pro Prozess genau einmal zulässt. Eine zweite Deklaration desselben Namens ist ein fataler Fehler. Composer löst deshalb jedes Paket in genau einer Version auf und meldet einen Konflikt, wenn sich die Versionsbedingungen nicht vereinbaren lassen. Das hat einen erwünschten Nebeneffekt: Die Fehlermeldung erreicht diejenigen, die eine zu enge Versionsbedingung lockern können, und das ist der Hauptgrund dafür, dass Abhängigkeitsbäume in PHP klein bleiben. Das PHAR von PHPUnit kann daran nicht teilnehmen, weil es seine Abhängigkeiten in einen Prozess mitbringt, dessen Abhängigkeitsbaum es nie gesehen hat. Seit PHPUnit 8.3 benennt es sie deshalb beim Build mit php-scoper um, aus SebastianBergmann\Exporter wird PHPUnitPHAR\SebastianBergmann\Exporter.
Mehr dazu im Artikel: Eine Version pro Prozess
Kann ich meine Tests schon gegen die nächste, noch unveröffentlichte PHP-Version laufen lassen?
Ja, und PHPUnit ist die Voraussetzung dafür. Ich teste PHPUnit gegen eine neue PHP-Version, sobald deren Entwicklung beginnt. Die Arbeit an PHP 8.6 startete am 23. September 2025, und seit dem 26. September 2025 wird jede Änderung an jedem Branch von PHPUnit auch gegen PHP 8.6 getestet. Erst wenn PHPUnit mit den Entwicklungsversionen von PHP funktioniert, können andere Projekte ihre eigenen Testsuites dagegen laufen lassen. Die GitHub Action setup-php von Shivam Mathur stellt dafür auch Nightly Builds von PHP bereit. Umgekehrt testet das PHP-Projekt jede Nacht unter anderem Laravel, Symfony, WordPress und PHPUnit gegen seine Entwicklungsversion.
Mehr dazu im Artikel: Wie PHP und sein Ökosystem sich gegenseitig testen
Kann ich ein Jahrzehnt auf PHP setzen?
Ich halte die Antworten von PHP auf die Fragen, nach denen eine Architekturentscheidung tatsächlich fällt, für stark, aber nicht für vorbehaltlos. Niemand kann PHP umlizenzieren. Ab PHP 8.6 steht es unter Version 4 der PHP License, und das ist BSD-3-Clause ohne weitere Bedingungen. Das Urheberrecht ist auf Hunderte Beitragende verteilt, und es gibt keine Organisation, die sich übernehmen ließe. Seit zehn Jahren erscheint jedes Jahr im November eine neue Version. Jede bekommt zwei Jahre aktiven Support und zwei weitere Jahre Sicherheitsupdates, und die Upgrades sind meist mechanisch. Dem stehen zwei Risiken gegenüber: Ein großer Teil der bezahlten Wartung hängt an einer schrumpfenden Zahl von Sponsoren, und über Änderungen an der Sprache stimmen tatsächlich nur etwa 140 Personen ab.
Mehr dazu im Artikel: Auch eine Sprache hat nichtfunktionale Anforderungen
Schulungen und Beratung
Kannst du mein Team in PHPUnit schulen?
Ja. Alle Schulungen gibt es auch nur für dein Team, online oder bei euch vor Ort, von „Testautomation mit PHPUnit“ bis „PHPUnit für Fortgeschrittene“. Die Übersicht der Schulungen zeigt alle Kurse und ihre nächsten offenen Termine.
Kannst du helfen, eine bestehende, schwer testbare Codebasis testbar zu machen?
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. Wie das abläuft, beschreibe ich unter Beratung und Coaching.
Hilfst du uns, Quality Gates für die Arbeit mit Coding-Agenten aufzubauen?
Ja, das ist neben PHPUnit derzeit einer meiner beiden Schwerpunkte. Wenn ein LLM-basierter Coding-Agent den Code schreibt, entscheiden Tests, die festlegen, was der Code tun soll, statische Analyse und Reviews darüber, was davon ins Repository kommt. Wie ich dabei vorgehe, beschreibe ich unter Beratung und Coaching.
Hilfst du uns bei Supply Chain Security?
Ja, das ist mein zweiter Schwerpunkt neben PHPUnit. Ich helfe Teams, Composer-Abhängigkeiten aktuell zu halten, ohne jedem Update blind zu vertrauen, ihre GitHub Actions zu härten und nachzuweisen, was in ihrer Software steckt, wie es ISO/IEC 18974 und der Cyber Resilience Act verlangen. Mehr dazu unter Beratung und Coaching.