Ursprünglich habe ich diesen Artikel am 6. Februar 2025 als Newsletter an meine Abonnentinnen und Abonnenten verschickt. Weitere Informationen findest du am Ende dieses Artikels.
PHPUnit 13
PHPUnit 13 ist ein wichtiger Meilenstein in der Entwicklung des beliebtesten Testframeworks für PHP. Diese neue Major Version, die am 6. Februar 2026 rausgekommen ist, folgt einer bewährten Entwicklungsphilosophie: Jedes Jahr im Februar kommt eine neue Major Version von PHPUnit raus, die als jährlicher Checkpoint für die Beseitigung technischer Schulden dient.
Dieser Release-Zyklus bietet eine strategische Gelegenheit, abgekündigte Funktionen, deren Support-Zeit abgelaufen ist, zu entfernen und den Support für PHP-Versionen einzustellen, die vom PHP-Projekt selbst nicht mehr aktiv gepflegt werden.
PHPUnit 13 bringt wichtige Änderungen an den Basisvoraussetzungen mit sich, indem die Unterstützung für PHP 8.3 eingestellt und PHP 8.4 als Mindestversion festgelegt wird. Dies steht im Einklang mit dem Support-Lebenszyklus von PHP und stellt sicher, dass die Benutzerinnen und Benutzer von PHPUnit von den neuesten PHP-Features profitieren.
Es ist jedoch wichtig zu beachten, dass die Veröffentlichung von PHPUnit 13 frühere Versionen nicht sofort überflüssig macht, da sie in bestehenden Projekten weiterhin funktionieren werden. Auch wenn für diese älteren Versionen irgendwann keine Bugfixes mehr bereitgestellt werden, werde ich mich dafür einsetzen, dass sie so lange wie möglich mit neu veröffentlichten PHP-Versionen kompatibel bleiben.
Die Architektur des Release-Zyklus von PHPUnit trennt bewusst die Aufgaben von Major Versions und Minor Versions:
Major Versions, die jedes Jahr im Februar rauskommen, bieten die grundlegende Infrastruktur für das kommende Jahr. Sie konzentrieren sich hauptsächlich auf Wartungsarbeiten, wie zum Beispiel die Bereinigung der Codebasis durch das Entfernen veralteter APIs und die Sicherstellung der Kompatibilität mit PHP-Versionen.
Minor Versions, die im Laufe des Jahres rauskommen, bieten neue Features und Funktionen, die die Funktionalität von PHPUnit erweitern.
Während Major Versions normalerweise keine auffälligen Neuerungen enthalten, ist PHPUnit 13 eine bemerkenswerte Ausnahme. Diese Version führt mehrere neue Funktionen ein, von denen mindestens eine eine genauere Betrachtung verdient, da sie einen bedeutenden Fortschritt in der Art und Weise darstellt, wie wir das Testen von modernen PHP-Anwendungen angehen.
Das Legacy-Problem
PHPUnit hat schon lange Test Doubles unterstützt und damit Entwicklerinnen und Entwicklern mächtige Tools gegeben, um die Interaktionen zwischen Objekten zu überprüfen. Eines dieser Tools war die Methode withConsecutive(), mit wir festlegen konnten, dass eine Methode auf einem Mock Object mehrmals aufgerufen werden sollte, und zwar mit unterschiedlichen Parametern für jeden Aufruf. Diese Methode hatte aber grundlegende Designbeschränkungen, was dazu führte, dass sie in PHPUnit 9.6 abgekündigte und in PHPUnit 10 entfernt wurde.
Das Hauptproblem lag in der strengen sequenziellen Abgleichanforderung von withConsecutive() und der Unfähigkeit, dynamische Szenarien zu verarbeiten, in denen Parametersätze in einer unvorhersehbaren Reihenfolge eintreffen oder nur ein teilweiser Abgleich nötig ist.
Beim Upgrade auf PHPUnit 10 mussten Entwicklerinnen und Entwickler umständliche Workarounds mit willReturnCallback() und komplexer Closure-Logik implementieren. Oft waren Dutzende Zeilen Boilerplate-Code nötig, um einfache Szenarien zur Überprüfung auf aufeinanderfolgende Aufrufe zu replizieren.
Eine bessere Lösung
Der Bedarf an ausgeklügelteren Möglichkeiten zum Abgleichen von Parametern wurde in Issue #6407 angesprochen, was zur Implementierung von zwei speziellen Regeln im Mock Object-System von PHPUnit führte, wie in Pull Request #6455 beschrieben. Diese neuen Regeln bieten mehr Kontrolle darüber, wie Mock Objects Parametersätze über mehrere Aufrufe hinweg validieren, wodurch die mit withConsecutive() verbundenen Probleme beseitigt werden.
Die unten gezeigten Beispiele verwenden die Schnittstelle Dispatcher sowie die Klassen Service, AnEvent und AnotherEvent aus Issue #6407.
Erwarten von Parametersätzen in einer bestimmten Reihenfolge
Die Regel withParameterSetsInOrder() deckt den ursprünglichen Anwendungsfall von withConsecutive() ab und bietet mehr Zuverlässigkeit und klarere Semantik. Sie checkt, ob eine Methode Parameter in einer bestimmten Reihenfolge bekommt, wobei jeder Aufruf mit einem vordefinierten Parametersatz in genau der Reihenfolge abgeglichen wird, in der sie angegeben sind:
$dispatcher = $this->createMock(Dispatcher::class); $dispatcher ->expects($this->exactly(2)) ->method('dispatch') ->withParameterSetsInOrder( [new AnEvent], [new AnotherEvent], ); $service = new Service($dispatcher); $service->doSomething();
Die wichtigste Verbesserung ist die interne Umsetzung der Regel, die den Aufrufstatus zuverlässig beibehält und klarere Fehlermeldungen gibt, wenn Erwartungen nicht erfüllt werden. Anstatt sich auf eine fragile Array-Indizierung zu verlassen, nutzt die Regel einen speziellen Matcher, um die Anzahl der Aufrufe und die Parameterüberprüfung separat zu verfolgen. Dadurch werden die Race Conditions und Statusänderungsprobleme beseitigt, die withConsecutive() beeinträchtigt haben.
Erwarten von Parametersätzen in beliebiger Reihenfolge
Die Regel withParameterSetsInAnyOrder() führt eine neue Funktion für das Mock Object-System in PHPUnit ein. Sie überprüft, ob alle angegebenen Parametersätze während der Testausführung verwendet werden, unabhängig von ihrer Reihenfolge:
$dispatcher = $this->createMock(Dispatcher::class); $dispatcher ->expects($this->exactly(2)) ->method('dispatch') ->withParameterSetsInAnyOrder( [new AnotherEvent], [new AnEvent], ); $service = new Service($dispatcher); $service->doSomething();
Das betrifft Fälle, in denen asynchrone Operationen, parallele Verarbeitung oder nicht deterministische Ausführungsabläufe eine sequenzielle Überprüfung mit withParameterSetsInOrder() unpraktisch machen würden.
Die Verwendung von withParameterSetsInOrder(), wenn die Reihenfolge nicht wichtig ist, führt zu unnötiger Kopplung zwischen dem Testcode und den Implementierungsdetails. Das führt zu einer Instabilität der Tests: Wenn wir den Produktionscode umgestalten, um Operationen neu zu ordnen, ohne das beobachtbare Verhalten zu ändern, schlagen die Tests fehl, obwohl die Funktionalität weiterhin korrekt ist. Solche Fehler führen zu falsch-negativen Ergebnissen, was das Vertrauen in die Testsuite untergräbt und den Wartungsaufwand erhöht.
Im Gegensatz dazu konzentriert sich withParameterSetsInAnyOrder() auf die Überprüfung dessen, was wichtig ist, nämlich dass alle erwarteten Interaktionen stattgefunden haben, und bleibt dabei unempfindlich gegenüber Änderungen in der Implementierung. Dieser Ansatz passt zum Testprinzip, das Verhalten statt die Implementierung zu überprüfen, und erzeugt Tests, die als zuverlässige Regressionsdetektoren dienen, ohne künstliche Einschränkungen für die Codeentwicklung aufzuerlegen. Verwende withParameterSetsInOrder() für Szenarien, in denen die sequenzielle Ausführung in einer bestimmten Reihenfolge für die Korrektheit wichtig ist.
Implementierungsdetails
Die Regeln lassen sich über einen sauberen Erweiterungspunkt in die bestehende InvocationMocker-Infrastruktur von PHPUnit integrieren. Jede Regel implementiert die ParameterSetMatcher-Schnittstelle. Durch dieses Design können die Regeln an der Verifizierungsphase von PHPUnit teilnehmen, wo sie sicherstellen können, dass alle erwarteten Parametersätze richtig verwendet wurden.
Die Implementierung behebt mehrere technische Probleme aus der Zeit von withConsecutive(). Beispielsweise behält jede Regelinstanz einen unabhängigen Status bei, und Fehlermeldungen geben genau an, welcher Parametersatz bei welchem Aufrufindex nicht übereinstimmt.
Migrationspfad
Für Entwicklerinnen und Entwickler, die gerade Callback-basierte Workarounds nutzen, ist der Migrationspfad ziemlich einfach. Das Beispiel unten zeigt das gängigste Muster, bei dem willReturnCallback() mit eingebetteten Assertions verwendet wird:
<?php declare(strict_types=1); $dispatcher = $this->createMock(Dispatcher::class); $matcher = $this->exactly(2); $expectedCalls = [ [new AnEvent], [new AnotherEvent] ]; $dispatcher ->expects($matcher) ->method('dispatch') ->willReturnCallback( function ($event) use ($matcher, $expectedCalls) { $invocationIndex = $matcher->numberOfInvocations() - 1; $expectedEvent = $expectedCalls[$invocationIndex][0]; $this->assertEquals( $expectedEvent, $event, sprintf( 'Expected event at invocation %d does not match', $invocationIndex + 1, ), ); } ); $service = new Service($dispatcher); $service->doSomething();
Wenn du das oben gezeigte Beispiel so umschreibst, dass es withParameterSetsInOrder() statt willReturnCallback() benutzt, sieht es genau so aus wie das Beispiel für withParameterSetsInOrder(), das ich dir vorhin gezeigt habe.
Einfluss auf das Ökosystem
Diese neuen Matcher machen die Mocking-Fähigkeiten von PHPUnit viel besser. Der regelbasierte Ansatz schafft Erweiterungspunkte für benutzerdefinierte Parameterabgleichslogik, sodass Teams domänenspezifische Validierungsregeln festlegen können, die sich nahtlos in die Test Double-Funktionalität von PHPUnit integrieren lassen.
Die Eliminierung von Callback-Workarounds reduziert die technischen Schulden und verbessert die Tests, indem der Overhead der Closure-Ausführung und der manuellen Assertion-Verwaltung vermieden wird. Diese Funktion beseitigt auch ein erhebliches Hindernis für die Einführung von PHPUnit 10 und neueren Versionen, da die Entfernung der Methode withConsecutive() mehrfach als primäres Hindernis identifiziert worden war.
Die Einführung von withParameterSetsInOrder() und withParameterSetsInAnyOrder() ist mehr als nur ein API-Ersatz. Sie schafft die Grundlage für Mocking-Funktionen der nächsten Generation, die Ausdruckskraft, Zuverlässigkeit und Wartbarkeit in modernen PHP-Testverfahren in den Vordergrund stellen.
Danke, Christina!
Die neuen Parameterabgleichsfunktionen, die in diesem Artikel besprochen werden, wurden von Christina Koenig entwickelt. Während einer meiner Schulungen hat sie eine gute Frage gestellt, die direkt zu Issue #6407 geführt hat. Sie hat einen echten Schwachpunkt im Test-Workflow aufgezeigt, mit dem viele Entwicklerinnen und Entwickler seit der Entfernung von withConsecutive() zu kämpfen hatten.
Anstatt einfach die bestehenden Workarounds zu akzeptieren, hat Christina nach Abschluss der Schulung die Initiative ergriffen und sich mit den Interna von PHPUnit beschäftigt. Sie hat sich selbstständig mit den Details der Implementierung der Test Double-Funktionalität auseinandergesetzt, sich mit den Designmustern der Codebasis vertraut gemacht und schließlich eine robuste Lösung in Pull Request #6455 entwickelt.
Ihr Beitrag zeigt nicht nur technische Kompetenz, sondern auch die Art von selbstständigem Lernen und Problemlösen, die Open Source vorantreibt. Christinas Arbeit wird die Testerfahrung für alle Nutzerinnen und Nutzer von PHPUnit direkt verbessern und eine häufig genannte Migrationsbarriere in eine Chance für verbesserte Funktionalität verwandeln.
Livestream
Hier ist die Aufzeichnung des Livestreams, in dem ich die wichtigsten Änderungen in PHPUnit 13 erläutert habe:
Exklusive Einblicke direkt in deinem Posteingang
Zeitgleich mit jedem Feature-Release von PHPUnit alle zwei Monate liefere ich eine umfassende Analyse der neuen Funktionen, Implementierungsdetails und strategischen Überlegungen hinter jeder Verbesserung direkt an meine Abonnentinnen und Abonnenten. Es ist mehr als nur ein ChangeLog: Es ist dein Tor zum Verständnis, wie modernste Testfunktionen deinen Entwicklungs-Workflow verändern können.
Einen Monat, nachdem meine Abonnentinnen und Abonnenten den Newsletter erhalten haben, veröffentliche ich den Inhalt hier auf dieser Website. Melde dich jetzt an, um sicherzustellen, dass du wertvolle Informationen über PHPUnit so schnell wie möglich erhältst.