@test
assert
PHPUnit · Magento 2 · Integration Tests · Docker
Magento Integration Tests einrichten und ausführen
von phpunit.xml bis zur Datenbankisolation

Wer Magento-Module ohne Integrationstests entwickelt, merkt Regressionen erst im Staging oder in der Produktion. Das Magento Test Framework bietet eine vollständige Umgebung mit echter Datenbankisolation, automatischer Fixture-Verwaltung und dem gesamten Magento-DI-Container – vorausgesetzt, man richtet es korrekt ein.

15 Min. Lesezeit phpunit.xml · ObjectManager · @magentoDbIsolation · Docker Magento 2.4 · PHP 8.4 · PHPUnit 10

1. Warum Integrationstests in Magento unverzichtbar sind

Magento 2 ist ein komplex vernetztes System: Ein einziger Aufruf eines Repositories durchläuft Plugins, Preferences, Event-Observer und mehrere Datenbankoperationen. Unit-Tests mit gemockten Dependencies können dieses Zusammenspiel nicht abbilden – sie testen die Teile, aber nicht das Ganze. Integrationstests hingegen booten den vollständigen Magento-DI-Container, verbinden sich mit einer echten Testdatenbank und führen Code im selben Kontext aus wie die Produktion.

Der praktische Nutzen zeigt sich bei Datenbankmigrationen, komplexen Repositoryabfragen und Events: Erst ein Integrationstest beweist, dass ein neu angelegtes Produkt tatsächlich indexiert, gespeichert und per SKU wieder abrufbar ist. Wer nur Unit-Tests hat, entdeckt solche Fehler im Code-Review oder nach dem Deploy – beides ist teurer als ein fehlschlagender Test in der CI-Pipeline. Das Magento Test Framework macht diesen Schutz erreichbar, erfordert aber eine einmalige, sorgfältige Einrichtung.

2. Aufbau der Magento Test-Infrastruktur

Magento liefert das Test Framework unter dev/tests/integration/ mit. Dort liegt eine eigene Bootstrap-Datei (framework/bootstrap.php), die den Magento-Kernel initialisiert, die Testdatenbank anlegt und die DI-Container-Konfiguration lädt. Die Testdatenbank ist vollständig von der Produktions- oder Entwicklungsdatenbank getrennt: Magento legt beim ersten Testlauf eine eigene Datenbank an (konfigurierbar per install-config-mysql.php) und rollt nach jedem Test alle Schreiboperationen per Transaktion zurück.

Die Verzeichnisstruktur für eigene Integrationstests folgt der Magento-Konvention: Testklassen liegen unter dev/tests/integration/testsuite/Vendor/Module/. Fixtures werden in gleichnamigen Unterverzeichnissen als PHP-Skripte abgelegt. Die Bootstrap-Umgebungsvariablen (TESTS_BASE_URL, TESTS_MAGENTO_MODE) steuern, ob Tests im Frontend- oder Backend-Kontext laufen. Diese Struktur ist nicht optional – Magento-Integrationstests funktionieren nur mit dem mitgelieferten Bootstrap, nicht mit dem Standard-PHPUnit-Bootstrap.

3. phpunit.xml für Integrationstests konfigurieren

Die Basiskonfigurationsdatei liegt unter dev/tests/integration/phpunit.xml.dist und muss nach phpunit.xml kopiert werden. Die wichtigsten Anpassungen betreffen das <testsuite>-Element: Für eigene Module trägt man dort den Pfad zu den eigenen Testklassen ein, statt alle Magento-Tests mitlaufen zu lassen. Das reduziert die Testlaufzeit drastisch – von Stunden auf Minuten. Die Umgebungsvariable TESTS_CLEANUP auf used gesetzt sorgt dafür, dass die Testdatenbank bei jedem Lauf auf den Ausgangszustand zurückgesetzt wird.

<!-- dev/tests/integration/phpunit.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
         bootstrap="framework/bootstrap.php"
         colors="true"
         beStrictAboutOutputDuringTests="true">

  <testsuites>
    <!-- Only run our own module tests, not all of Magento -->
    <testsuite name="Mironsoft Integration Tests">
      <directory>testsuite/Mironsoft</directory>
    </testsuite>
  </testsuites>

  <php>
    <ini name="date.timezone" value="Europe/Berlin"/>
    <const name="TESTS_INSTALL_CONFIG_FILE" value="etc/install-config-mysql.php"/>
    <const name="TESTS_GLOBAL_CONFIG_DIR" value="../../../app/etc"/>
    <const name="TESTS_CLEANUP" value="used"/>
    <const name="TESTS_BASE_URL" value="http://mironsoft.test/"/>
    <const name="TESTS_MAGENTO_MODE" value="developer"/>
    <const name="TESTS_ERROR_LOG_CLEAR" value="1"/>
  </php>

  <source>
    <include>
      <directory suffix=".php">../../../app/code/Mironsoft</directory>
    </include>
  </source>
</phpunit>

Die Datei etc/install-config-mysql.php enthält die Datenbankzugangsdaten für die Testdatenbank – niemals die Produktionsdatenbank. In einer Docker-Umgebung nach Mark-Shust-Schema lautet der Hostname db, der Benutzer magento und die Datenbank-Namen-Konvention lautet magento_test. Wer diese Datei korrekt pflegt, stellt sicher, dass Integrationstests nie auf Produktionsdaten zugreifen und das Aufsetzen einer neuen Entwicklungsumgebung reproduzierbar ist.

4. Die erste Integrationstestklasse schreiben

Eine Magento-Integrationstestklasse erbt von Magento\TestFramework\TestCase\AbstractController (für Controller-Tests) oder direkt von PHPUnit\Framework\TestCase kombiniert mit dem Magento\TestFramework\Helper\Bootstrap-Trait. Der Unterschied zum Unit-Test: Es wird kein Mock-Container aufgebaut, sondern der echte Magento-DI-Container ist verfügbar. Das bedeutet, alle Plugins, Preferences und Compilierungsartefakte müssen aktuell sein – ein häufiger Stolperstein beim ersten Einrichten.

Testmethoden sind reguläre PHPUnit-Methoden mit dem Attribut #[Test] oder der Annotation @test. Magento-spezifische Annotations wie @magentoDbIsolation enabled, @magentoDataFixture und @magentoConfigFixture steuern das Verhalten des Test-Frameworks vor und nach jedem Test. Diese Annotations werden vom Magento-Bootstrap ausgewertet, nicht von PHPUnit selbst – was erklärt, warum sie nur in korrekter Magento-Testumgebung funktionieren.

<?php

declare(strict_types=1);

namespace Mironsoft\Catalog\Test\Integration;

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Api\Data\ProductInterfaceFactory;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;

/**
 * Integration test for custom product repository behavior.
 */
class ProductSaveTest extends TestCase
{
    private ProductRepositoryInterface $productRepository;
    private ProductInterfaceFactory $productFactory;

    protected function setUp(): void
    {
        parent::setUp();
        $objectManager = Bootstrap::getObjectManager();
        $this->productRepository = $objectManager->get(ProductRepositoryInterface::class);
        $this->productFactory    = $objectManager->get(ProductInterfaceFactory::class);
    }

    /**
     * @test
     * @magentoDbIsolation enabled
     * @magentoAppIsolation enabled
     */
    public function productIsSavedAndRetrievableBySku(): void
    {
        $product = $this->productFactory->create();
        $product->setSku('test-integration-sku-001')
                ->setName('Integration Test Product')
                ->setTypeId('simple')
                ->setAttributeSetId(4)
                ->setPrice(19.99);

        $saved = $this->productRepository->save($product);

        $this->assertNotEmpty($saved->getId());

        $loaded = $this->productRepository->get('test-integration-sku-001');
        $this->assertSame('Integration Test Product', $loaded->getName());
        $this->assertEquals(19.99, (float) $loaded->getPrice());
    }
}

5. ObjectManager im Test nutzen und DI-Bootstrap

In Integrationstests ist die Verwendung des ObjectManagers explizit erlaubt und erwünscht – im Gegensatz zu Produktionscode, wo der ObjectManager niemals direkt aufgerufen werden sollte. Bootstrap::getObjectManager() gibt den vollständig initialisierten DI-Container zurück, der alle compilierten Factory- und Proxy-Klassen kennt. Das erlaubt es, echte Instanzen von Repositorys, Services und Factories zu erzeugen, wie es der Produktionscode tun würde.

Wichtig: Der ObjectManager im Test teilt denselben Zustand mit dem Magento-Bootstrap – das bedeutet, Konfigurationsänderungen, die per @magentoConfigFixture gesetzt werden, sind im ObjectManager sofort sichtbar. Bei @magentoAppIsolation enabled wird der ObjectManager zwischen Tests zurückgesetzt, was verhindert, dass gecachte Instanzen aus einem Test die Ergebnisse des nächsten beeinflussen. Diese Isolation hat jedoch ihren Preis: Jeder Testlauf mit App-Isolation dauert länger, weil der Container neu aufgebaut wird.

6. Datenbankisolation mit @magentoDbIsolation

Die Annotation @magentoDbIsolation enabled ist das Herzstück des Magento Test Frameworks. Sie aktiviert für die markierte Testklasse oder -methode eine Datenbanktransaktion, die nach dem Test automatisch zurückgerollt wird. Das bedeutet: Jeder Test beginnt mit demselben Datenbankzustand, egal in welcher Reihenfolge die Tests laufen oder was der vorherige Test in die Datenbank geschrieben hat. Dieses Verhalten macht Integrationstests deterministisch – eine Eigenschaft, die ohne Isolation kaum zu erreichen wäre.

Die Isolation hat Grenzen: DDL-Operationen (Tabellenänderungen), Operationen auf MyISAM-Tabellen und externe Systeme wie Elasticsearch oder Redis werden nicht zurückgerollt. Wer Tests schreibt, die den Suchindex oder den Cache beeinflussen, muss diese nach dem Test manuell aufräumen oder @magentoAppIsolation enabled kombinieren. Die Annotation kann auf Klassen- oder Methodenebene gesetzt werden – auf Klassenebene gilt sie für alle Methoden der Klasse als Standard.

7. Tests im Docker-Container ausführen

Im Mark-Shust-Docker-Setup läuft PHPUnit über bin/phpunit, das den Befehl im PHP-Container ausführt. Der entscheidende Unterschied zur lokalen Ausführung: Im Container sind alle Datenbankverbindungen korrekt konfiguriert, die PHP-Extensions (xdebug, sodium) sind vorhanden und der Magento-Bootstrap findet alle nötigen Konfigurationsdateien. Wer PHPUnit lokal auf dem Host-System ausführt, scheitert fast immer an Verbindungsfehlern zur Testdatenbank oder fehlenden PHP-Extensions.

# Execute integration tests for a specific module
# Run from project root — bin/phpunit uses the PHP container
bin/phpunit -c dev/tests/integration/phpunit.xml \
  --testsuite "Mironsoft Integration Tests" \
  --filter "ProductSaveTest" \
  --colors=always \
  -v

# Run all integration tests with coverage (slow — only for CI)
bin/phpunit -c dev/tests/integration/phpunit.xml \
  --coverage-html dev/tests/integration/coverage \
  --coverage-filter app/code/Mironsoft

# Run single test method
bin/phpunit -c dev/tests/integration/phpunit.xml \
  "dev/tests/integration/testsuite/Mironsoft/Catalog/Test/Integration/ProductSaveTest.php" \
  --filter productIsSavedAndRetrievableBySku

# Rebuild generated code before testing (mandatory after class changes)
bin/magento setup:di:compile && \
  bin/phpunit -c dev/tests/integration/phpunit.xml

Die Ausführungszeit ist der größte Unterschied zu Unit-Tests: Ein einzelner Integrationstest mit Datenbankzugriff dauert typischerweise 1–5 Sekunden. Bei hundert Tests summiert sich das auf mehrere Minuten. Der empfohlene Workflow: Während der Entwicklung einzelne Tests per --filter ausführen, in der CI-Pipeline alle Tests der Suite. Die TESTS_CLEANUP=used-Option stellt sicher, dass die Testdatenbank nur dann neu aufgebaut wird, wenn sich die Installationskonfiguration geändert hat.

8. Typische Fehler und deren Behebung

Der häufigste Fehler beim ersten Einrichten: "Could not connect to the database". Ursache ist fast immer eine falsche install-config-mysql.php – entweder falsche Credentials, falscher Hostname (auf dem Host-System localhost, im Container db) oder eine nicht existierende Testdatenbank. Die Lösung: Datenbankverbindung im Container explizit testen mit bin/cli mysql -h db -u magento -p magento_test.

Ein zweiter häufiger Fehler: "Class not found" für Factory- oder Proxy-Klassen. Das passiert, wenn setup:di:compile nicht ausgeführt wurde oder die generierte Code-Verzeichnis leer ist. Magento Integrationstests benötigen die kompilierten DI-Artefakte, auch im Developer-Modus. Ein dritter Fehler: Tests, die sich gegenseitig beeinflussen, weil @magentoDbIsolation fehlt. Das äußert sich in nicht-deterministischen Fehlern, die nur auftreten, wenn Tests in einer bestimmten Reihenfolge ausgeführt werden.

9. Unit- vs. Integrationstests: Was wann testen

Unit-Tests und Integrationstests ergänzen sich – sie ersetzen sich nicht gegenseitig. Die Entscheidung, welchen Typ man wählt, hängt davon ab, was man beweisen möchte. Unit-Tests beweisen, dass eine einzelne Klasse unter definierten Eingaben das richtige Ergebnis liefert – schnell, isoliert, ohne Datenbankzugriff. Integrationstests beweisen, dass das System als Ganzes funktioniert: Repositorys schreiben wirklich in die Datenbank, Events werden tatsächlich ausgelöst, Plugins greifen in der richtigen Reihenfolge ein.

Kriterium Unit-Test Integrationstest Wann verwenden
Geschwindigkeit Millisekunden 1–5 Sekunden Unit-Tests für schnelles Feedback
Datenbankzugriff Kein (gemockt) Echte Testdatenbank Integration für DB-Logik
Plugin-Verhalten Nicht testbar Vollständig testbar Integration für Plugins
Isolierung Vollständig isoliert Transaktions-Rollback @magentoDbIsolation enabled
Setup-Aufwand Minimal Einmalig hoch Lohnt sich ab erstem Modul

Die empfohlene Aufteilung in Magento-Projekten: Alle Berechnungs- und Transformationslogik in Klassen, die ohne DI-Container testbar sind, durch Unit-Tests absichern. Repository-Operationen, Observer, Plugins und komplexe Service-Ketten durch Integrationstests beweisen. Controller-Verhalten und Template-Ausgabe durch MFTF (Functional Tests) oder Playwright abdecken. Eine realistische Verteilung für ein mittelgroßes Modul: 60 % Unit-Tests, 30 % Integrationstests, 10 % Funktionstests.

Mironsoft

Magento 2 Entwicklung, Testing und Qualitätssicherung

Magento-Module mit vollständiger Testabdeckung entwickeln?

Wir richten die Integrationstestumgebung für euer Magento-Projekt ein, schreiben die ersten Tests für eure kritischen Module und integrieren PHPUnit in die CI/CD-Pipeline – sodass jede Änderung automatisch validiert wird.

Setup & Konfiguration

phpunit.xml, Testdatenbank und Bootstrap für eure Docker-Umgebung einrichten

Test-Implementierung

Integrationstests für Repositorys, Services und Plugins schreiben

CI-Integration

PHPUnit in GitHub Actions oder GitLab CI mit Testbericht-Artefakten integrieren

10. Zusammenfassung

Magento 2 Integrationstests mit PHPUnit einzurichten erfordert einmalig Aufwand, zahlt sich aber bei jedem weiteren Entwicklungszyklus aus. Die Kernkomponenten sind: eine korrekte phpunit.xml mit eigenem <testsuite>-Block, eine install-config-mysql.php mit Testdatenbankzugangsdaten, Testklassen unter dev/tests/integration/testsuite/Vendor/Module/ und die konsequente Verwendung von @magentoDbIsolation enabled. Im Docker-Container ausgeführt über bin/phpunit, laufen Tests in derselben Umgebung wie der Produktionscode.

Die wichtigste Erkenntnis: Integrationstests sind keine Luxus, sondern Versicherung. Wer ein Plugin schreibt, das ein Repository-Verhalten verändert, kann nur mit einem Integrationstest beweisen, dass das Plugin im Zusammenspiel mit allen anderen Plugins, dem ORM und der Datenbank korrekt funktioniert. Unit-Tests können diesen Beweis nicht erbringen. Die Investition in die Einrichtung der Testumgebung ist einmalig – der Nutzen kommt bei jedem Refactoring, jeder Erweiterung und jeder Magento-Version-Aktualisierung zurück.

Magento Integration Tests — Das Wichtigste auf einen Blick

Konfiguration

phpunit.xml mit eigenem Testsuite-Block und install-config-mysql.php mit Testdatenbank-Credentials – niemals Produktions-DB.

Datenbankisolation

@magentoDbIsolation enabled rollt alle DB-Operationen nach dem Test zurück – macht Tests deterministisch und unabhängig von Ausführungsreihenfolge.

ObjectManager

In Tests explizit erlaubt: Bootstrap::getObjectManager() gibt den vollständigen DI-Container zurück – echte Instanzen statt Mocks.

Ausführung

bin/phpunit im Docker-Container ausführen. Vor dem ersten Lauf bin/magento setup:di:compile – generierter Code ist Pflicht für Tests.

11. FAQ: Magento Integration Tests mit PHPUnit

1Brauche ich eine separate Testdatenbank?
Ja, unbedingt. Magento legt beim ersten Testlauf eine eigene Testdatenbank an. Niemals Produktions- oder Entwicklungsdatenbank als Testdatenbank konfigurieren.
2Warum schlägt der Test mit "Class not found" fehl?
Factory- und Proxy-Klassen brauchen bin/magento setup:di:compile. Nach jeder Klassen- oder DI-Änderung muss di:compile neu ausgeführt werden.
3Darf ich ObjectManager in Tests verwenden?
Ja – in Integrationstests ist Bootstrap::getObjectManager() das empfohlene Werkzeug. Im Produktionscode ist er verboten, im Testcode erlaubt.
4Was macht @magentoAppIsolation enabled?
Setzt den DI-Container nach dem Test zurück. Teurer als DB-Isolation, aber notwendig wenn gecachte Singletons Tests beeinflussen.
5Kann ich Tests lokal ohne Docker ausführen?
Technisch ja, aber nicht empfohlen. Im Docker-Container sind DB, PHP-Version und Extensions identisch zur CI-Pipeline.
6Wie lange dauern Integrationstests?
1–5 Sekunden pro Test. Bootstrap-Zeit ca. 10–30 Sekunden. 50 Tests: 3–10 Minuten. Während der Entwicklung --filter verwenden.
7Wie setze ich Konfigurationswerte für Tests?
@magentoConfigFixture current_store path/to/config value – wirkt für den Test, wird danach zurückgesetzt. Global-Scope mit @magentoConfigFixture global.
8Werden Elasticsearch-Operationen zurückgerollt?
Nein. Nur MySQL-Transaktionen. Elasticsearch, Redis und Dateisystem-Änderungen bleiben – manuell aufräumen oder @magentoAppIsolation verwenden.
9Wie teste ich einen Plugin mit Integrationstest?
Nach di:compile ist das Plugin automatisch aktiv. Original-Interface per ObjectManager holen und aufrufen – der Interceptor greift transparent ein.
10Integration Tests in GitHub Actions Pipeline?
Docker-Compose-Stack starten, Magento installieren, dann bin/phpunit mit Integration-phpunit.xml. JUnit-XML-Artefakt für GitHub-Test-Reporter speichern.