mit PHPUnit-Tests absichern
Mehrere Plugins auf derselben Methode haengen still von der sortOrder in di.xml ab. Ein Test, der die tatsaechliche Ausfuehrungsreihenfolge dokumentiert, verhindert, dass ein neues Plugin unbemerkt eine bestehende Reihenfolge und damit das Verhalten kippt.
Inhaltsverzeichnis
- 1. Warum Plugin-Reihenfolge ein unsichtbares, aber kritisches Verhalten ist
- 2. Ein Beispiel-Szenario mit drei konkurrierenden Plugins
- 3. Die tatsaechliche Ausfuehrungsreihenfolge mit einem Integrationstest pruefen
- 4. Einen einfachen Execution-Recorder als wiederverwendbaren Testbaustein bauen
- 5. Alternativ: Reihenfolge indirekt ueber das Endergebnis pruefen
- 6. Regressionsschutz beim Hinzufuegen eines vierten Plugins
- 7. Besonderheiten bei gemischten before- und after-Plugins beruecksichtigen
- 8. Den Test gleichzeitig als lesbare Dokumentation der Reihenfolge nutzen
- 9. Plugin-Reihenfolge-Tests strategisch in die Test-Suite einordnen
- 10. Zusammenfassung
- 11. FAQ
1. Warum Plugin-Reihenfolge ein unsichtbares, aber kritisches Verhalten ist
Magentos Plugin-System erlaubt es, dass mehrere Module dieselbe Methode derselben Klasse abfangen, gesteuert ausschliesslich durch die sortOrder in den jeweiligen di.xml-Dateien. Diese Reihenfolge ist an keiner zentralen Stelle sichtbar dokumentiert, sondern ergibt sich implizit aus der Summe aller Module, die auf dieselbe Methode zugreifen. Fuer 'before'-Plugins bestimmt eine niedrigere sortOrder eine frühere Ausfuehrung, fuer 'after'-Plugins kehrt sich die Wirkung auf das Endergebnis faktisch um, da spaeter ausgefuehrte 'after'-Plugins den Rueckgabewert der vorherigen ueberschreiben koennen.
Das Risiko wird konkret, sobald ein zweites Team oder ein drittes Modul ein weiteres Plugin auf dieselbe Methode registriert. Ohne bewusste Abstimmung landet die sortOrder zufaellig irgendwo, und ein Plugin, das eigentlich vor der Preisberechnung laufen sollte, laeuft ploetzlich danach. Das Ergebnis ist ein Bug, der sich nicht durch fehlerhaften Code in einem einzelnen Plugin erklaeren laesst, sondern nur durch das Zusammenspiel mehrerer Plugins in der falschen Reihenfolge, ein Fehlerbild, das besonders muehsam zu debuggen ist.
2. Ein Beispiel-Szenario mit drei konkurrierenden Plugins
Zur Veranschaulichung dient ein Preis-Berechnungsdienst mit drei Plugins: ein Rabatt-Plugin, das einen prozentualen Rabatt abzieht, ein Steuer-Plugin, das die Mehrwertsteuer aufschlaegt, und ein Rundungs-Plugin, das den finalen Preis auf zwei Nachkommastellen rundet. Die fachlich korrekte Reihenfolge ist eindeutig: erst Rabatt, dann Steuer, zuletzt Rundung. Wuerde die Rundung vor der Steuerberechnung laufen, entstuenden durch doppeltes Runden minimale, aber reale Centabweichungen im finalen Preis.
In der di.xml wird diese Reihenfolge ueber drei sortOrder-Werte festgelegt, zum Beispiel 10 fuer den Rabatt, 20 fuer die Steuer und 30 fuer die Rundung. Diese Zahlen allein sagen einem neuen Entwickler aber nichts ueber die fachliche Notwendigkeit dieser Reihenfolge, sie sehen wie eine willkuerliche Nummerierung aus. Genau hier setzt ein Test an, der die tatsaechliche Ausfuehrungsreihenfolge explizit macht und damit auch die fachliche Begruendung dokumentiert.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Mironsoft\Pricing\Model\PriceCalculator">
<plugin name="mironsoft_discount" type="Mironsoft\Pricing\Plugin\ApplyDiscountPlugin" sortOrder="10" />
<plugin name="mironsoft_tax" type="Mironsoft\Pricing\Plugin\ApplyTaxPlugin" sortOrder="20" />
<plugin name="mironsoft_rounding" type="Mironsoft\Pricing\Plugin\RoundPricePlugin" sortOrder="30" />
</type>
</config>
3. Die tatsaechliche Ausfuehrungsreihenfolge mit einem Integrationstest pruefen
Da Plugin-Verdrahtung ein Interceptor-Mechanismus ist, der von Magentos generiertem Code getragen wird, laesst sich die reale Reihenfolge nur zuverlaessig ueber einen Integrationstest pruefen, der die tatsaechliche, durch den Object Manager erzeugte Klasse aufruft. Ein Unit-Test mit direkt instanziierter Klasse wuerde die Plugins komplett umgehen und damit genau das nicht testen, worauf es ankommt.
Der Trick fuer einen aussagekraeftigen Reihenfolge-Test ist ein Logging-Mechanismus: Jedes Plugin traegt beim Durchlaufen seinen eigenen Namen in ein gemeinsames Protokoll ein, zum Beispiel ueber ein injiziertes, im Test austauschbares Logger-Objekt. Nach dem Aufruf der Methode prueft der Test, ob die Namen exakt in der erwarteten Reihenfolge im Protokoll stehen. Das macht die Ausfuehrungsreihenfolge zu einer expliziten, versionierten Testaussage statt einer stillen Annahme in drei separaten di.xml-Dateien.
<?php
declare(strict_types=1);
namespace Mironsoft\Pricing\Test\Integration\Model;
use Magento\TestFramework\Helper\Bootstrap;
use Mironsoft\Pricing\Model\PriceCalculator;
use Mironsoft\Pricing\Test\Fixture\PluginExecutionRecorder;
use PHPUnit\Framework\TestCase;
class PriceCalculatorPluginOrderTest extends TestCase
{
public function testPluginsRunInDiscountTaxRoundingOrder(): void
{
$objectManager = Bootstrap::getObjectManager();
PluginExecutionRecorder::reset();
/** @var PriceCalculator $calculator */
$calculator = $objectManager->create(PriceCalculator::class);
$calculator->calculate(100.0);
self::assertSame(
['mironsoft_discount', 'mironsoft_tax', 'mironsoft_rounding'],
PluginExecutionRecorder::getExecutionOrder()
);
}
}
4. Einen einfachen Execution-Recorder als wiederverwendbaren Testbaustein bauen
Der Execution-Recorder aus dem vorherigen Beispiel ist bewusst simpel gehalten: eine statische Klasse mit einem Array, in das jedes teilnehmende Plugin seinen Namen eintraegt, sobald es durchlaufen wird. Statische Zustaende sind in Unit-Tests grundsaetzlich mit Vorsicht zu geniessen, aber fuer diesen sehr eng begrenzten Zweck, eine Reihenfolge innerhalb eines einzelnen Testlaufs zu protokollieren, sind sie ein pragmatisches und leicht verstaendliches Werkzeug.
Wichtig ist, den Recorder vor jedem Test explizit zurueckzusetzen, damit Ergebnisse aus vorherigen Tests nicht faelschlich in den aktuellen Test hineinwirken. Jedes Plugin ruft den Recorder in seiner beforeCalculate() oder afterCalculate()-Methode auf, was in Produktion keinerlei Overhead verursacht, wenn der Recorder-Aufruf nur im Testkontext oder ueber ein Null-Object-Pattern aktiviert wird.
<?php
declare(strict_types=1);
namespace Mironsoft\Pricing\Test\Fixture;
class PluginExecutionRecorder
{
/** @var string[] */
private static array $order = [];
public static function reset(): void
{
self::$order = [];
}
public static function record(string $pluginName): void
{
self::$order[] = $pluginName;
}
/**
* @return string[]
*/
public static function getExecutionOrder(): array
{
return self::$order;
}
}
5. Alternativ: Reihenfolge indirekt ueber das Endergebnis pruefen
Nicht jedes Team moechte einen dedizierten Recorder in Produktionscode einbauen, selbst wenn er nur im Testkontext aktiv ist. Eine Alternative ist, die Reihenfolge indirekt ueber das fachlich korrekte Endergebnis zu pruefen: Bei einem Preis von hundert Euro, zehn Prozent Rabatt und neunzehn Prozent Steuer ergibt die korrekte Reihenfolge (erst Rabatt, dann Steuer) ein anderes Endergebnis als die falsche Reihenfolge (erst Steuer, dann Rabatt).
Dieser Ansatz ist weniger explizit als der Execution-Recorder, weil ein Testversagen nicht sofort zeigt, welche zwei Plugins vertauscht wurden, sondern nur, dass irgendetwas an der Berechnung nicht stimmt. Er hat aber den Vorteil, komplett ohne Testinfrastruktur im Produktionscode auszukommen und pruft gleichzeitig, ob die Berechnung insgesamt fachlich korrekt ist, nicht nur, ob die Reihenfolge formal stimmt.
<?php
declare(strict_types=1);
namespace Mironsoft\Pricing\Test\Integration\Model;
use Magento\TestFramework\Helper\Bootstrap;
use Mironsoft\Pricing\Model\PriceCalculator;
use PHPUnit\Framework\TestCase;
/**
* @magentoConfigFixture default_store mironsoft_pricing/discount/percent 10
* @magentoConfigFixture default_store tax/calculation/rate 19
*/
class PriceCalculatorResultOrderTest extends TestCase
{
public function testDiscountAppliedBeforeTaxProducesExpectedTotal(): void
{
$objectManager = Bootstrap::getObjectManager();
/** @var PriceCalculator $calculator */
$calculator = $objectManager->create(PriceCalculator::class);
$result = $calculator->calculate(100.0);
// 100 - 10% discount = 90.00, then + 19% tax = 107.10
self::assertSame(107.10, $result);
}
}
6. Regressionsschutz beim Hinzufuegen eines vierten Plugins
Der eigentliche Wert eines Reihenfolge-Tests zeigt sich, sobald ein weiteres Team ein viertes Plugin auf dieselbe Methode registriert, etwa ein Treuepunkte-Plugin, das zusaetzliche Punkte basierend auf dem finalen Preis berechnet. Ohne Reihenfolge-Test faellt eine falsch gewaehlte sortOrder, die das Treuepunkte-Plugin versehentlich vor die Rundung setzt, moeglicherweise erst in der Produktion auf, wenn Punkteberechnungen minimal von den ungerundeten statt den gerundeten Preisen abweichen.
Mit einem bestehenden Reihenfolge-Test schlaegt der Test sofort fehl, sobald das neue Plugin registriert wird, weil die erwartete Reihenfolge im Assert nicht mehr mit der tatsaechlichen uebereinstimmt. Der Entwickler des neuen Plugins wird damit gezwungen, die sortOrder bewusst zu waehlen und den Test explizit zu aktualisieren, statt sich zufaellig irgendwo in die Kette einzureihen. Der Test wird so zu einem aktiven Kommunikationskanal zwischen Teams, die sonst nichts voneinander wissen.
<?php
declare(strict_types=1);
namespace Mironsoft\Pricing\Test\Integration\Model;
use Magento\TestFramework\Helper\Bootstrap;
use Mironsoft\Pricing\Model\PriceCalculator;
use Mironsoft\Pricing\Test\Fixture\PluginExecutionRecorder;
use PHPUnit\Framework\TestCase;
class PriceCalculatorPluginOrderWithLoyaltyTest extends TestCase
{
public function testLoyaltyPluginRunsAfterRounding(): void
{
$objectManager = Bootstrap::getObjectManager();
PluginExecutionRecorder::reset();
/** @var PriceCalculator $calculator */
$calculator = $objectManager->create(PriceCalculator::class);
$calculator->calculate(100.0);
self::assertSame(
['mironsoft_discount', 'mironsoft_tax', 'mironsoft_rounding', 'mironsoft_loyalty_points'],
PluginExecutionRecorder::getExecutionOrder()
);
}
}
7. Besonderheiten bei gemischten before- und after-Plugins beruecksichtigen
Die Reihenfolge wird komplexer, sobald sowohl 'before'- als auch 'after'-Plugins auf derselben Methode aktiv sind, da Magento fuer 'before'-Plugins die sortOrder aufsteigend, aber fuer 'after'-Plugins de facto in der Reihenfolge anwendet, in der sie den Rueckgabewert weiterreichen, was bei niedrigerer sortOrder zuerst den inneren, bei hoeherer sortOrder den aeusseren Wrapper bildet. Ein Test, der nur 'around'-Plugins betrachtet, deckt dieses Verhalten nicht ab.
Fuer gemischte Szenarien lohnt es sich, im Recorder explizit zu protokollieren, ob ein Eintrag aus einer 'before'- oder 'after'-Methode stammt, etwa als 'mironsoft_discount:before' und 'mironsoft_discount:after'. So macht der Test sichtbar, in welcher Reihenfolge sowohl der Eintritt als auch der Austritt aus jedem Plugin erfolgt, was insbesondere bei Plugins wichtig ist, die den Rueckgabewert nachtraeglich veraendern.
8. Den Test gleichzeitig als lesbare Dokumentation der Reihenfolge nutzen
Ueber die reine Regressionssicherung hinaus hat ein Reihenfolge-Test einen zweiten Wert: Er ist die einzige Stelle im Code, an der die fachliche Notwendigkeit einer bestimmten Plugin-Reihenfolge explizit als Kommentar und als Assertion nebeneinander stehen. Waehrend die sortOrder-Werte in den verschiedenen di.xml-Dateien fuer sich genommen keine Begruendung tragen, kann der Test-Docblock genau erklaeren, warum Rabatt vor Steuer und Steuer vor Rundung laufen muss.
Diese Doppelrolle macht den Test zu einem wertvollen Onboarding-Werkzeug fuer neue Teammitglieder: Statt drei verschiedene di.xml-Dateien in drei verschiedenen Modulen zu durchsuchen, um die Gesamtlogik zu verstehen, liefert ein Blick in die Testdatei sofort die komplette Kette samt Begruendung. Diese Eigenschaft rechtfertigt den Aufwand fuer den Reihenfolge-Test auch dann, wenn ein Team aktuell keine akuten Reihenfolge-Bugs hat.
9. Plugin-Reihenfolge-Tests strategisch in die Test-Suite einordnen
Da Plugin-Reihenfolge-Tests zwingend Integrationstests sind, sie brauchen den echten, generierten Interceptor, sind sie langsamer als reine Unit-Tests, aber immer noch deutlich schneller als ein kompletter End-to-End-Test ueber die REST-API. Sie gehoeren in die Integrationstest-Phase der CI-Pipeline und sollten bei jeder Aenderung an einer di.xml-Datei mit betroffenen Plugins ausgefuehrt werden.
Ein pragmatischer Ansatz ist, Reihenfolge-Tests gezielt fuer alle Methoden anzulegen, auf denen drei oder mehr Plugins aus unterschiedlichen Modulen aktiv sind, da hier das Risiko einer unbeabsichtigten sortOrder-Kollision am hoechsten ist. Fuer Methoden mit nur einem einzigen Plugin ist ein solcher Test unnoetiger Aufwand, da es dort per Definition keine Reihenfolge zu testen gibt.
| Testansatz | Was er zeigt | Vorteil | Nachteil |
|---|---|---|---|
| Execution-Recorder | Exakte Reihenfolge aller Plugin-Aufrufe | Sofort erkennbar, welche zwei Plugins vertauscht sind | Braucht zusaetzlichen Test-Baustein im Code |
| Ergebnis-basierter Test | Fachlich korrektes Endergebnis der Berechnung | Keine Testinfrastruktur im Produktionscode noetig | Fehlerursache bei Fehlschlag weniger konkret |
| before/after getrennt protokolliert | Ein- und Austrittsreihenfolge bei gemischten Plugin-Typen | Deckt auch around- und after-Wrapper-Verhalten ab | Etwas aufwendiger im Recorder-Setup |
| Data-Provider mit mehreren Startwerten | Reihenfolge-Korrektheit ueber mehrere Eingaben hinweg | Deckt Rundungs- und Grenzfaelle zusaetzlich ab | Mehr Testfaelle zu pflegen |
Mironsoft
Testautomatisierung, Magento-Qualitätssicherung und CI-Integration
Tests, die echte Fehler finden statt nur grün zu leuchten?
Wir prüfen bestehende PHPUnit-Suiten auf Implementierungsdetail-Tests, flaky Tests und fehlende Coverage an kritischen Stellen und bauen daraus eine Teststrategie, die bei jedem Magento-Update wirklich Sicherheit gibt.
Test-Audit
Bestehende Suiten auf Mocking-Antipatterns und blinde Flecken prüfen.
Teststrategie
Unit-, Integrations- und MFTF-Tests sinnvoll für Magento-Projekte kombinieren.
CI-Integration
Schnelle, zuverlässige Testläufe in GitLab CI oder GitHub Actions einrichten.
10. Zusammenfassung
Plugin-Reihenfolge-Tests: Das Wichtigste auf einen Blick
Kernidee
Tatsaechliche Plugin-Ausfuehrungsreihenfolge ueber einen Integrationstest mit Execution-Recorder explizit machen.
Warum Unit-Test nicht reicht
Plugins wirken nur ueber den generierten Interceptor, den ein direkt instanziiertes Objekt umgeht.
Zusatznutzen
Der Test dokumentiert gleichzeitig die fachliche Begruendung der Reihenfolge fuer neue Teammitglieder.
Wann besonders wichtig
Bei drei oder mehr Plugins aus unterschiedlichen Modulen auf derselben Methode.