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.
Inhaltsverzeichnis
- 1. Warum Integrationstests in Magento unverzichtbar sind
- 2. Aufbau der Magento Test-Infrastruktur
- 3. phpunit.xml für Integrationstests konfigurieren
- 4. Die erste Integrationstestklasse schreiben
- 5. ObjectManager im Test nutzen und DI-Bootstrap
- 6. Datenbankisolation mit @magentoDbIsolation
- 7. Tests im Docker-Container ausführen
- 8. Typische Fehler und deren Behebung
- 9. Unit- vs. Integrationstests: Was wann testen
- 10. Zusammenfassung
- 11. FAQ
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.