@test
assert
Magento 2 · MFTF · PHPUnit · API Tests · Test-Strategie
MFTF vs. PHPUnit vs. API Tests:
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.

17 Min. Lesezeit MFTF · PHPUnit Unit · Integration · REST API · Testpyramide Magento 2.4.8 · PHP 8.4 · PHPUnit 11

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.

9. FAQ: MFTF vs. PHPUnit vs. API Tests in Magento 2

1Was ist MFTF in Magento 2?
Browser-basiertes End-to-End-Framework auf Selenium-Basis. Tests in XML, prüft JavaScript und CSS. Langsam und wartungsintensiv – nur für echte Browser-Interaktionen.
2Wann MFTF statt PHPUnit?
Nur bei echten Browser-Interaktionen: Checkout-Flow, JavaScript-Validierungen, Alpine.js-Verhalten. Für Logik und DB immer PHPUnit.
3Was testen API-Tests?
HTTP-Schicht: Statuscodes, JSON-Schema, Auth, Paginierung. Nicht die Geschäftslogik – die gehört in Unit und Integrationstests.
4Wie lange dauern Integrationstests?
1-10 Minuten pro Modul. Bootstrap: 30-60 Sekunden. In CI-Pipelines auf separate Jobs auslagern.
5MFTF für Hyvä-Themes?
Eingeschränkt. Luma-Selektoren passen nicht zu Hyvä. Playwright ist für JavaScript-heavy Frontends wie Hyvä die modernere Alternative.
6Typische Test-Aufteilung für ein Modul?
70-80% Unit Tests, 15-20% Integrationstests, 3-5% API-Tests, 1-5% MFTF. Invertierte Pyramide → langsame, fragile CI-Pipeline.
7Unit Test vs. Integrationstest abgrenzen?
Constructor-Injection ohne Framework-Aufrufe → Unit Test. DB- oder DI-Container-Zugriff → Integrationstest. Separate phpunit.xml-Dateien.
8API-Tests in Magento einrichten?
dev/tests/api-functional, phpunit_rest.xml. Laufende Magento-Instanz erforderlich. Basisklasse WebApiAbstract für HTTP-Assertions.
9MFTF oder Playwright für Hyvä?
Playwright für neue Projekte: bessere JS-Unterstützung, schneller, modernere API. MFTF hat bessere Magento-Fixture-Integration, aber Luma-Selektoren.
10Welche Tests bei Commit, welche nightly?
Commit/PR: Unit Tests (Sekunden). Separate Stage: Integrationstests (Minuten). Nightly: API-Tests und MFTF. Schnelles Feedback bei jedem Commit, vollständige Coverage nightly.