Was wann passt
In Magento 2 gibt es drei grundlegend verschiedene Test-Typen mit sehr unterschiedlichen Laufzeiten, Wartungskosten und Einsatzbereichen. PHPUnit für Unit- und Integrationstests, MFTF für End-to-End-Browser-Tests und REST-API-Tests für die Webservice-Schicht. Die häufigsten Fehler in Magento-Test-Strategien: zu viele MFTF-Tests, zu wenige Unit Tests, und API-Tests, die eigentlich Logik-Tests sein sollten.
Inhaltsverzeichnis
- 1. Die Magento-Testpyramide
- 2. PHPUnit Unit Tests: Logik ohne Framework
- 3. PHPUnit Integrationstests: Bootstrap mit Datenbank
- 4. REST-API-Tests: Webservice-Schicht isoliert prüfen
- 5. MFTF: Browser-End-to-End-Tests im Überblick
- 6. Entscheidungsbaum: Welcher Test für welches Problem?
- 7. Die vier Test-Typen im direkten Vergleich
- 8. Zusammenfassung
- 9. FAQ
1. Die Magento-Testpyramide
Mike Cohns Testpyramide gilt auch für Magento: Viele schnelle, billige Unit Tests bilden die Basis. Weniger Integrationstests in der Mitte. Noch weniger langsame, teure End-to-End-Tests an der Spitze. In Magento 2 gibt es vier konkrete Testtypen, die dieser Pyramide folgen sollten: PHPUnit Unit Tests (Sekunden, kein Bootstrap), PHPUnit Integrationstests (Minuten, vollständiger Magento-Stack mit DB), REST-API-Tests (Minuten, echter HTTP-Stack), und MFTF-Tests (viele Minuten bis Stunden, Browser + Selenium).
In der Praxis sieht man häufig invertierte Pyramiden: Teams haben viele MFTF-Tests, die beim geringsten UI-Änderung brechen, kaum Integrationstests und keine Unit Tests. Das Ergebnis ist eine langsame, fragile CI-Pipeline, die mehr Zeit mit dem Reparieren von Tests verbringt als mit dem Schreiben neuer Features. Die richtige Aufteilung für ein Magento-Modul mittlerer Größe: 70-80% Unit Tests, 15-20% Integrationstests, 5-10% API- oder MFTF-Tests für kritische End-to-End-Pfade.
2. PHPUnit Unit Tests: Logik ohne Framework
PHPUnit-Unit-Tests sind in Magento 2 die schnellste Feedback-Schleife. Sie werden über dev/tests/unit/phpunit.xml konfiguriert und laufen ohne Magento-Bootstrap. Die Laufzeit für eine vollständige Modul-Testsuite liegt typischerweise unter 5 Sekunden. Diese Tests eignen sich für alle Klassen, die ihre Abhängigkeiten per Constructor erhalten: ViewModels, Service Classes, einfache Plugins, Preisberechnungslogik, Validatoren und DataTransformer.
Das entscheidende Kriterium für einen Unit Test ist, dass alle Abhängigkeiten durch Mocks ersetzt werden können. Wenn eine Klasse einen Datenbankaufruf macht, muss das Repository-Interface gemockt werden, nicht die Implementierung. Wenn eine Klasse einen HTTP-Aufruf macht, muss der HTTP-Client gemockt werden. Wenn diese Mocks so aufwändig werden, dass der Test selbst kaum noch lesbar ist, ist das ein Signal: Entweder ist die Klasse zu groß (Refactoring), oder ein Integrationstest ist die richtige Wahl.
<?php
// tests/Unit/Service/DiscountCalculatorTest.php — pure unit test, no bootstrap
declare(strict_types=1);
namespace Tests\Unit\Service;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Mironsoft\Sales\Service\DiscountCalculator;
use Mironsoft\Sales\Api\CustomerTierRepositoryInterface;
class DiscountCalculatorTest extends TestCase
{
#[DataProvider('discountScenarioProvider')]
public function testCalculatesCorrectDiscount(
string $tier,
float $subtotal,
float $expectedDiscount
): void {
$tierRepo = $this->createMock(CustomerTierRepositoryInterface::class);
$tierRepo->method('getByCustomerGroup')->willReturn(
$this->buildTier($tier, ['bronze' => 5.0, 'silver' => 10.0, 'gold' => 15.0][$tier] ?? 0.0)
);
$calculator = new DiscountCalculator($tierRepo);
$discount = $calculator->calculate($subtotal, 'customer_group_' . $tier);
$this->assertEqualsWithDelta($expectedDiscount, $discount, 0.01);
}
public static function discountScenarioProvider(): array
{
return [
'bronze 100€' => ['bronze', 100.00, 5.00],
'silver 200€' => ['silver', 200.00, 20.00],
'gold 500€' => ['gold', 500.00, 75.00],
'unknown tier' => ['guest', 100.00, 0.00],
];
}
private function buildTier(string $name, float $rate): object
{
return new class($name, $rate) {
public function __construct(
public readonly string $name,
public readonly float $discountRate
) {}
};
}
}
3. PHPUnit Integrationstests: Bootstrap mit Datenbank
PHPUnit-Integrationstests in Magento laufen über dev/tests/integration/phpunit.xml und initialisieren den vollständigen Magento-Stack inklusive Datenbankverbindung, DI-Container und Event-System. Die Laufzeit liegt im Minutenbereich – typisch 1-10 Minuten für eine Modul-Testsuite, abhängig von der Anzahl der Fixture-Setups und DB-Operationen. Magento bietet für Integrationstests ein Fixtures-System mit #[Fixture]-Attributen, die Datenbankzustände vor einem Test herstellen und nach dem Test zurückrollen.
Integrationstests sind der richtige Testtyp für Repository-Implementierungen (die echten SQL-Abfragen testen), für Observer-Verhalten (das den Event-Dispatch voraussetzt), für Plugin-Ketten (die den DI-Container und die Interceptor-Generierung brauchen) und für Setup-Patches (die Datenbankschemas anlegen). Ein häufiger Fehler ist es, Integrationstests für Logik zu schreiben, die auch per Unit Test testbar wäre – das erhöht die Laufzeit unnötig und macht die Tests anfälliger für Infrastrukturprobleme.
<?php
// dev/tests/integration/testsuite/Mironsoft/Sales/Model/DiscountRepositoryTest.php
declare(strict_types=1);
namespace Mironsoft\Sales\Model;
use Magento\TestFramework\Helper\Bootstrap;
use Magento\TestFramework\Fixture\DbIsolation;
use PHPUnit\Framework\TestCase;
use Mironsoft\Sales\Api\DiscountRepositoryInterface;
use Mironsoft\Sales\Api\Data\DiscountInterface;
#[DbIsolation(enabled: true)]
class DiscountRepositoryTest extends TestCase
{
private DiscountRepositoryInterface $repository;
protected function setUp(): void
{
$objectManager = Bootstrap::getObjectManager();
$this->repository = $objectManager->get(DiscountRepositoryInterface::class);
}
public function testSavesAndRetrievesDiscount(): void
{
/** @var DiscountInterface $discount */
$discount = Bootstrap::getObjectManager()->create(DiscountInterface::class);
$discount->setCode('TEST10');
$discount->setRate(10.0);
$discount->setIsActive(true);
$saved = $this->repository->save($discount);
$retrieved = $this->repository->getByCode('TEST10');
$this->assertSame($saved->getId(), $retrieved->getId());
$this->assertEqualsWithDelta(10.0, $retrieved->getRate(), 0.001);
$this->assertTrue($retrieved->isActive());
}
public function testThrowsOnDuplicateCode(): void
{
$this->expectException(\Magento\Framework\Exception\AlreadyExistsException::class);
foreach (range(1, 2) as $_) {
$discount = Bootstrap::getObjectManager()->create(DiscountInterface::class);
$discount->setCode('DUPLICATE');
$discount->setRate(5.0);
$this->repository->save($discount);
}
}
}
4. REST-API-Tests: Webservice-Schicht isoliert prüfen
Magento 2 bietet eine vollständige REST-API und ein dediziertes Test-Framework für WebAPI-Tests unter dev/tests/api-functional. Diese Tests verwenden den echten HTTP-Stack von Magento, treffen aber den PHP-Prozess direkt – kein Browser, kein Selenium. Sie sind ideal, um zu prüfen, ob REST-Endpunkte korrekte HTTP-Statuscodes zurückgeben, ob Authentifizierung und Autorisierung funktionieren, ob API-Antworten das korrekte JSON-Schema haben und ob Paginierung und Filter korrekt umgesetzt sind.
API-Tests sind schneller als MFTF-Tests, aber langsamer als Integrationstests, weil sie den HTTP-Request-Response-Zyklus durchlaufen. Ihr Einsatzgebiet ist klar abgegrenzt: die Webservice-Schicht. Ob die zugrundeliegende Geschäftslogik korrekt ist, sollte durch Unit- und Integrationstests abgedeckt sein. API-Tests prüfen, ob diese Logik korrekt über HTTP exponiert wird – nicht die Logik selbst. Das vermeidet doppelte Coverage und hält die API-Test-Suite klein und schnell.
5. MFTF: Browser-End-to-End-Tests im Überblick
Das Magento Functional Testing Framework (MFTF) basiert auf Selenium und steuert einen echten Browser. MFTF-Tests sind XML-basiert und beschreiben Benutzerinteraktionen: Formular ausfüllen, Button klicken, Assertion auf sichtbaren Text. Sie sind der einzige Testtyp, der das vollständige Frontend-Rendering inklusive JavaScript, Alpine.js-Komponenten und CSS-Interaktionen testet.
Der Preis für diese Vollständigkeit ist hoch: MFTF-Tests sind typischerweise 10-100x langsamer als Integrationstests, brechen bei jedem UI-Refactoring und erfordern eine komplette laufende Magento-Instanz mit Browser-Treiber. In Hyvä-Projekten kommt hinzu, dass das Standard-MFTF-Framework für Luma ausgelegt ist – Hyvä-spezifische Alpine.js-Interaktionen erfordern eigene MFTF-Anpassungen oder Playwright-basierte Alternativlösungen. MFTF sollte auf kritische End-to-End-Pfade beschränkt bleiben: Checkout-Komplettszenario, Login-Flow, Warenkorb-Grundfunktionen.
6. Entscheidungsbaum: Welcher Test für welches Problem?
Die Entscheidung für einen Test-Typ folgt einer einfachen Frage-Kette: Testet man Logik in einer einzelnen Klasse? Unit Test. Testet man das Zusammenspiel mehrerer Klassen mit Datenbank? Integrationstest. Testet man einen HTTP-Endpunkt ohne Browser? API-Test. Testet man eine vollständige Benutzerinteraktion im Browser? MFTF. Jede Antwort schließt die anderen Optionen weitgehend aus – der einzige sinnvolle Grund, von dieser Regel abzuweichen, ist wenn ein Test-Typ für das spezifische Problem technisch nicht umsetzbar ist.
Ein häufiger Fehler in Magento-Projekten: Man schreibt MFTF-Tests für Checkout-Preisberechnungen, weil man "sicher gehen" will. Das Ergebnis ist ein langsamer, fragiler Test, der bei jeder CSS-Klassen-Änderung im Template bricht, obwohl die Preislogik selbst korrekt ist. Die Preislogik gehört in Unit Tests (Berechnungsalgorithmus) und Integrationstests (Datenbankinteraktion mit echten Produkten und Preisregeln). Der MFTF-Test prüft nur, ob der Gesamtprozess funktioniert – und zwar genau einen Happy-Path.
7. Die vier Test-Typen im direkten Vergleich
Jeder Test-Typ hat eine klar definierte Rolle in der Magento-Test-Strategie. Die Auswahl hängt davon ab, was getestet werden soll, nicht davon, was technisch möglich ist.
| Kriterium | Unit Test | Integrationstest | API-Test | MFTF |
|---|---|---|---|---|
| Laufzeit | ms–Sek. | Minuten | Minuten | 10min–Std. |
| Bootstrap | Nein | Vollständig | HTTP-Stack | Browser + Magento |
| Testet | Klassen-Logik | DB + DI-Container | HTTP-Endpunkte | Browser-Interaktion |
| Wartungsaufwand | Gering | Mittel | Mittel | Hoch |
| Empfohlener Anteil | 70–80% | 15–20% | 3–5% | 1–5% |
Die empfohlenen Anteile sind keine starren Vorgaben, sondern Orientierungswerte für ein gesundes Test-Portfolio. In einem Modul, das hauptsächlich Datenbankoperationen kapselt, wird der Anteil von Integrationstests höher sein. In einem Modul mit komplexer Preisberechnungslogik und einfachem DB-Schema wird der Unit-Test-Anteil höher sein. Die MFTF-Tests bleiben in jedem Fall auf kritische Happy-Paths beschränkt.
8. Zusammenfassung
Die Magento-Test-Strategie folgt der allgemeinen Testpyramide, angepasst an die vier Magento-spezifischen Test-Typen. PHPUnit-Unit-Tests bilden die Basis: schnell, deterministisch, ohne Bootstrap, für alle Klassen mit mockbaren Abhängigkeiten. PHPUnit-Integrationstests testen das Zusammenspiel mit der Datenbank und dem DI-Container – Repository-Implementierungen, Observer-Ketten und Plugin-Sequenzen. REST-API-Tests prüfen die HTTP-Schnittstelle, nicht die Logik dahinter. MFTF deckt kritische End-to-End-Browser-Pfade ab und wird bewusst klein gehalten.
Der größte Hebel für eine robuste Magento-Testsuite ist die konsequente Aufteilung nach diesem Muster. Jeder MFTF-Test, der eigentlich ein Logik-Test ist, bindet Ressourcen in der CI-Pipeline und verlangsamt Feedback. Jeder Unit Test, der durch einen Integrationstest ersetzt wird, erhöht die Laufzeit unnötig. Das richtige Werkzeug für das richtige Problem – diese Regel ist bei keinem anderen Aspekt von Magento-Qualitätssicherung so wichtig wie bei der Test-Typ-Wahl.
MFTF vs. PHPUnit vs. API Tests — Das Wichtigste auf einen Blick
Testpyramide einhalten
70-80% Unit Tests, 15-20% Integrationstests, maximal 5% MFTF. Invertierte Pyramide bedeutet langsame, fragile CI-Pipeline.
MFTF auf Happy-Paths beschränken
Checkout-Komplettszenario, Login-Flow. Keine MFTF-Tests für Preislogik oder Datenbankoperationen – die gehören in Unit- und Integrationstests.
API-Tests für HTTP-Schicht
REST-Endpunkte, HTTP-Statuscodes, Auth, Paginierung – nicht die Geschäftslogik dahinter. Die gehört in Unit Tests.
Entscheidungskriterium
Was wird getestet? Logik→Unit, DB+DI→Integration, HTTP→API, Browser→MFTF. Das richtige Werkzeug für das richtige Problem.