Multi-Store- und Multi-Website-Szenarien in Magento-Tests abbilden
AI generated
@test
assert
PHPUnit · Magento · Multi-Store
Multi-Store- und Multi-Website-Szenarien
realistisch in PHPUnit-Tests abbilden

Ein Store-Scope-Bug faellt oft erst live auf, weil Tests nur den Default-Store kennen. Wer ScopeConfigInterface gezielt mockt, deckt Store- und Website-Abweichungen ab, ohne fuer jeden Store echte Fixtures in der Datenbank zu pflegen.

15 Min. Lesezeit ScopeConfigInterface StoreManagerInterface Website-Scope

1. Warum Multi-Store-Bugs Tests so oft entwischen

Magento-Shops mit mehreren Stores oder Websites teilen sich Code, aber nicht zwangslaeufig Konfiguration. Steuerklassen, Versandarten, Preisregeln, Anzeige-Texte: fast jede Einstellung kann pro Store, pro Store-View oder pro Website ueberschrieben werden. Wer ein Modul nur gegen den Default-Store testet, prueft damit genau einen von vielen moeglichen Konfigurationszustaenden und uebersieht systematisch alles, was in einem zweiten Store abweicht.

Das Tueckische daran: Der Code funktioniert im Test einwandfrei, weil ScopeConfigInterface::getValue() ohne expliziten Scope-Parameter automatisch den aktuellen Default-Scope liest. Erst live, wenn ein Kunde ueber die zweite Website mit abweichender Waehrung oder abweichendem Steuersatz bestellt, zeigt sich, dass die Business-Logik den Store-Parameter gar nicht weiterreicht. Ein Test, der store-spezifische Werte gezielt mockt, deckt genau diese Luecke auf, bevor sie produktiv wird.

2. ScopeConfigInterface gezielt fuer mehrere Stores mocken

Der Kern jeder Multi-Store-Testsituation ist ein Mock von ScopeConfigInterface, der je nach uebergebenem Scope-Code unterschiedliche Werte liefert. Statt eines einzelnen willReturn() nutzt man willReturnCallback() oder willReturnMap(), um fuer jeden Store-Code einen eigenen Konfigurationswert zu hinterlegen. So laesst sich in einem einzigen Unit-Test simulieren, dass Store 'de' einen anderen Mindestbestellwert hat als Store 'at', ohne dass jemals eine echte Datenbank angefragt wird.

Wichtig ist, in der zu testenden Klasse selbst konsequent den Store-Code als Scope-Parameter durchzureichen, statt sich auf den impliziten aktuellen Scope zu verlassen. Genau das macht der folgende Test sichtbar: Er ruft die Business-Logik einmal mit Store 'de' und einmal mit Store 'at' auf und erwartet unterschiedliche Ergebnisse. Reicht die Klasse den Scope nicht korrekt weiter, liefert der Mock in beiden Faellen denselben Wert und der Test schlaegt fehl.


<?php
declare(strict_types=1);

namespace Mironsoft\Shipping\Test\Unit\Model;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;
use Mironsoft\Shipping\Model\MinimumOrderResolver;
use PHPUnit\Framework\TestCase;

class MinimumOrderResolverTest extends TestCase
{
    private ScopeConfigInterface $scopeConfigMock;
    private MinimumOrderResolver $resolver;

    protected function setUp(): void
    {
        $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class);
        $this->resolver = new MinimumOrderResolver($this->scopeConfigMock);
    }

    public function testMinimumOrderValueDiffersPerStore(): void
    {
        $this->scopeConfigMock
            ->method('getValue')
            ->with('mironsoft_shipping/general/minimum_order', ScopeInterface::SCOPE_STORE, $this->anything())
            ->willReturnMap([
                ['mironsoft_shipping/general/minimum_order', ScopeInterface::SCOPE_STORE, 'de', '50.00'],
                ['mironsoft_shipping/general/minimum_order', ScopeInterface::SCOPE_STORE, 'at', '80.00'],
            ]);

        self::assertSame(50.0, $this->resolver->getMinimumOrderValue('de'));
        self::assertSame(80.0, $this->resolver->getMinimumOrderValue('at'));
    }
}

3. StoreManagerInterface fuer mehrere Stores und Websites simulieren

Neben der Konfiguration selbst braucht ein realistischer Multi-Store-Test oft auch einen Mock von StoreManagerInterface, der mehrere Store-Objekte mit unterschiedlichen IDs, Codes und Website-Zugehoerigkeiten zurueckgibt. Das ist besonders relevant, wenn Code den aktuellen Store dynamisch ermittelt, statt ihn als Parameter zu erhalten, etwa in einem Plugin oder Observer, der auf getStore() zugreift.

In der Praxis bewaehrt es sich, dafuer eine kleine Test-Factory-Methode zu schreiben, die Store-Mocks mit den gewuenschten Eigenschaften erzeugt. So laesst sich pro Test genau festlegen, welche Stores zu welcher Website gehoeren, ohne den Aufbau jedes Mal von Hand zu wiederholen. Diese Factory-Methode wird schnell zu einem wiederverwendbaren Baustein fuer alle Tests, die store- oder website-abhaengiges Verhalten pruefen.


<?php
declare(strict_types=1);

namespace Mironsoft\Shipping\Test\Unit\Model;

use Magento\Store\Api\Data\StoreInterface;
use Magento\Store\Model\StoreManagerInterface;
use PHPUnit\Framework\TestCase;

trait StoreMockFactoryTrait
{
    private function createStoreMock(int $id, string $code, int $websiteId): StoreInterface
    {
        $storeMock = $this->createMock(StoreInterface::class);
        $storeMock->method('getId')->willReturn($id);
        $storeMock->method('getCode')->willReturn($code);
        $storeMock->method('getWebsiteId')->willReturn($websiteId);

        return $storeMock;
    }

    private function createStoreManagerMock(array $stores, int $currentStoreId): StoreManagerInterface
    {
        $storeManagerMock = $this->createMock(StoreManagerInterface::class);
        $storeManagerMock->method('getStores')->willReturn($stores);
        $storeManagerMock->method('getStore')->willReturnCallback(
            static fn (?int $storeId = null) => $stores[$storeId ?? $currentStoreId]
        );

        return $storeManagerMock;
    }
}

4. Warum Mocks hier den echten DB-Fixtures vorzuziehen sind

Der naheliegende Reflex fuer Multi-Store-Tests ist, in einem Integrationstest tatsaechlich mehrere Stores und Websites per Fixture-Skript anzulegen. Das funktioniert, ist aber teuer: Jeder zusaetzliche Store bedeutet zusaetzliche Datenbank-Setup-Zeit, zusaetzliche Aufraeumarbeit nach dem Test und eine staerkere Kopplung an den konkreten Zustand der Test-Datenbank. Bei zehn oder zwanzig Unit-Tests, die alle store-abhaengiges Verhalten pruefen, summiert sich das schnell zu einer spuerbar langsameren Test-Suite.

Ein gemockter ScopeConfigInterface und ein gemockter StoreManagerInterface liefern dieselbe Testaussage in Millisekunden statt Sekunden, weil keine Datenbank involviert ist. Fuer die reine Business-Logik, die auf Konfigurationswerte reagiert, reicht das vollstaendig aus. Echte Multi-Store-Fixtures bleiben sinnvoll fuer wenige, gezielte Integrationstests, die tatsaechlich pruefen, ob die Store-Konfiguration in der Datenbank korrekt hinterlegt ist, nicht fuer die Masse der Unit-Tests.

5. Default-Store als Fallback korrekt testen

Ein haeufiger Fehler in Multi-Store-Setups ist eine fehlerhafte Fallback-Logik: Ein Store-View erbt eigentlich vom Default-Wert, aber der Code prueft faelschlich nur den spezifischen Store-Scope und ignoriert, dass Magento bei fehlendem Store-Wert automatisch auf Website- und dann auf Default-Scope zurueckfaellt. Ein Test sollte deshalb explizit auch den Fall abdecken, dass fuer einen Store kein eigener Wert gesetzt ist und stattdessen der Website- oder Default-Wert greifen muss.

Dazu konfiguriert man den ScopeConfigInterface-Mock so, dass er fuer den spezifischen Store-Scope null zurueckgibt und nur auf Website- oder Default-Scope-Ebene einen Wert liefert. Reicht die getestete Klasse den Fallback nicht selbst durch, sondern verlaesst sich fälschlich darauf, dass immer ein Store-Wert existiert, deckt genau dieser Test die Luecke auf, bevor sie in Produktion zu leeren oder falschen Werten fuehrt.

6. Website-uebergreifende Logik testen: geteilte Kataloge und Kundenkonten

Websites in Magento teilen sich unter Umstaenden Kataloge oder Kundenkonten, je nach Konfiguration von 'Shared Catalog' und 'Shared Customer Accounts'. Business-Logik, die davon ausgeht, dass ein Kunde nur einer Website zugeordnet ist, kann in einem Setup mit geteilten Kundenkonten falsch reagieren. Ein Test sollte deshalb explizit auch den Fall abbilden, dass ein Kundenkonto ueber mehrere Websites hinweg gueltig ist.

Dafuer mockt man die entsprechende Konfiguration so, dass 'Shared Customer Accounts' aktiv ist, und prueft, ob die Business-Logik dann korrekt websiteuebergreifend agiert, etwa beim Zusammenfuehren von Bestellhistorien oder beim Pruefen von Rabattberechtigung. Ohne diesen Test bleibt eine implizite Annahme im Code unentdeckt, die erst bei einem Kunden auffaellt, der ueber zwei Websites hinweg einkauft.


<?php
declare(strict_types=1);

namespace Mironsoft\Loyalty\Test\Unit\Model;

use Magento\Customer\Api\Data\GroupInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Mironsoft\Loyalty\Model\CrossWebsiteEligibilityChecker;
use PHPUnit\Framework\TestCase;

class CrossWebsiteEligibilityCheckerTest extends TestCase
{
    public function testEligibleWhenSharedCustomerAccountsEnabled(): void
    {
        $scopeConfigMock = $this->createMock(ScopeConfigInterface::class);
        $scopeConfigMock->method('isSetFlag')
            ->with('customer/account_share/scope')
            ->willReturn(true);

        $checker = new CrossWebsiteEligibilityChecker($scopeConfigMock);

        self::assertTrue($checker->isEligibleAcrossWebsites());
    }

    public function testNotEligibleWhenAccountsAreWebsiteScoped(): void
    {
        $scopeConfigMock = $this->createMock(ScopeConfigInterface::class);
        $scopeConfigMock->method('isSetFlag')
            ->with('customer/account_share/scope')
            ->willReturn(false);

        $checker = new CrossWebsiteEligibilityChecker($scopeConfigMock);

        self::assertFalse($checker->isEligibleAcrossWebsites());
    }
}

7. Data-Provider fuer mehrere Store-Kombinationen nutzen

Statt fuer jeden Store einen eigenen Testmethodennamen wie testMinimumOrderForStoreDe() und testMinimumOrderForStoreAt() zu schreiben, buendelt ein Data-Provider alle Store-Kombinationen in einer einzigen parametrisierten Testmethode. Das reduziert Duplizierung erheblich und macht es einfach, einen weiteren Store einfach als zusaetzliche Zeile im Data-Provider zu ergaenzen, statt eine komplette neue Testmethode zu schreiben.

Besonders wertvoll wird das bei Modulen mit vielen Stores, etwa in international ausgerichteten Shops mit zehn oder mehr Store-Views. Ein Data-Provider mit Store-Code, erwartetem Konfigurationswert und erwartetem Ergebnis liefert auf einen Blick eine vollstaendige Uebersicht, welches Verhalten fuer welchen Store erwartet wird, und macht die Testdatei selbst zu einer Art lebender Dokumentation der Store-Unterschiede.


<?php
declare(strict_types=1);

namespace Mironsoft\Shipping\Test\Unit\Model;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class FreeShippingThresholdTest extends TestCase
{
    #[DataProvider('storeThresholdProvider')]
    public function testThresholdPerStore(string $storeCode, string $configValue, float $expected): void
    {
        $scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class);
        $scopeConfigMock->method('getValue')->willReturn($configValue);

        $resolver = new \Mironsoft\Shipping\Model\FreeShippingThreshold($scopeConfigMock);

        self::assertSame($expected, $resolver->getThreshold($storeCode));
    }

    public static function storeThresholdProvider(): array
    {
        return [
            'default store de' => ['de', '75.00', 75.0],
            'austria store at' => ['at', '90.00', 90.0],
            'switzerland store ch' => ['ch', '120.00', 120.0],
        ];
    }
}

8. Wann ein echter Multi-Store-Integrationstest trotzdem noetig ist

Trotz aller Vorteile des Mockings gibt es Faelle, in denen ein echter Integrationstest mit mehreren tatsaechlich angelegten Stores unverzichtbar bleibt: naemlich immer dann, wenn geprueft werden soll, ob Magento selbst die Scope-Aufloesung korrekt durchfuehrt, etwa bei komplexen Vererbungsketten zwischen Website- und Store-View-Konfiguration oder bei Indexer-Logik, die tatsaechlich store-spezifische Daten aus der Datenbank liest.

Der pragmatische Ansatz ist eine klare Aufgabenteilung: Unit-Tests mit gemockter Konfiguration decken die eigene Business-Logik ab und laufen in Millisekunden. Ein einzelner, bewusst schlanker Integrationstest mit einer @magentoConfigFixture-Annotation fuer den zweiten Store deckt zusaetzlich ab, dass die Konfiguration tatsaechlich so im System ankommt, wie die Unit-Tests es annehmen. Beide Ebenen ergaenzen sich, ersetzen sich aber nicht.


<?php
declare(strict_types=1);

namespace Mironsoft\Shipping\Test\Integration\Model;

use Magento\TestFramework\Helper\Bootstrap;
use Mironsoft\Shipping\Model\MinimumOrderResolver;
use PHPUnit\Framework\TestCase;

/**
 * @magentoConfigFixture default_store mironsoft_shipping/general/minimum_order 50.00
 * @magentoConfigFixture fixturestore_store mironsoft_shipping/general/minimum_order 80.00
 */
class MinimumOrderResolverIntegrationTest extends TestCase
{
    public function testResolverReadsRealStoreConfig(): void
    {
        $objectManager = Bootstrap::getObjectManager();
        /** @var MinimumOrderResolver $resolver */
        $resolver = $objectManager->create(MinimumOrderResolver::class);

        self::assertSame(80.0, $resolver->getMinimumOrderValue('fixturestore_store'));
    }
}

9. Multi-Store-Tests in die CI-Strategie einordnen

In einer CI-Pipeline sollten Multi-Store-Unit-Tests genauso schnell laufen wie jeder andere Unit-Test, da sie ausschliesslich gemockte Abhaengigkeiten nutzen. Sie gehoeren deshalb in dieselbe schnelle Testphase, die bei jedem Commit ausgefuehrt wird. Die wenigen echten Multi-Store-Integrationstests mit realen Fixtures dagegen gehoeren in eine separate, langsamere Phase, die etwa nur vor einem Merge in den Hauptbranch laeuft.

Diese Trennung verhindert, dass Entwickler versucht sind, Multi-Store-Tests aus Zeitgruenden ganz zu ueberspringen. Ein Team, das weiss, dass die schnellen Unit-Tests store-spezifisches Verhalten bereits gut absichern, kann bei der Anzahl der teuren Integrationstests bewusst sparsam bleiben, ohne dabei blinde Flecken in der Store-Abdeckung zu riskieren.

Szenario Testart Aufwand Wann sinnvoll
Store-spezifischer Konfigurationswert Unit-Test mit gemocktem ScopeConfigInterface Sehr gering Immer, als Standardfall
Fallback auf Website- oder Default-Scope Unit-Test mit gemocktem ScopeConfigInterface (null-Rueckgabe) Gering Bei jeder Fallback-Logik
Website-uebergreifende Kundenkonten Unit-Test mit gemocktem isSetFlag Gering Bei Shared-Customer-Account-Logik
Mehrere Stores gleichzeitig Data-Provider mit Store-Kombinationen Gering Ab drei oder mehr Stores
Echte Scope-Aufloesung durch Magento Integrationstest mit @magentoConfigFixture Hoch Nur fuer wenige, gezielte Faelle

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

Multi-Store-Tests: Das Wichtigste auf einen Blick

Kernidee

ScopeConfigInterface und StoreManagerInterface gezielt mocken statt echte Multi-Store-Fixtures aufzubauen.

Werkzeug

willReturnMap() oder willReturnCallback() fuer store-abhaengige Rueckgabewerte.

Haeufiger Fehler

Fallback auf Website- oder Default-Scope wird im Code nicht getestet und faellt live durch.

Grenze des Mockings

Echte Scope-Aufloesung durch Magento selbst braucht weiterhin einen schlanken Integrationstest.

11. FAQ: Multi-Store-Tests: Das Wichtigste auf einen Blick

1Muss ich fuer jeden Store einen echten Datensatz in der Test-Datenbank anlegen?
Nein, fuer die meisten Faelle reicht ein gemockter ScopeConfigInterface, der je nach Store-Code unterschiedliche Werte liefert. Echte Store-Fixtures lohnen sich nur fuer wenige gezielte Integrationstests.
2Wie mocke ich unterschiedliche Konfigurationswerte fuer mehrere Stores in PHPUnit?
Mit willReturnMap() auf dem ScopeConfigInterface-Mock, wobei jede Zeile aus Konfigurationspfad, Scope-Typ, Scope-Code und Rueckgabewert besteht. Alternativ eignet sich willReturnCallback() fuer komplexere Logik.
3Was ist der haeufigste Fehler bei Multi-Store-Tests?
Die zu testende Klasse verlaesst sich implizit auf den aktuellen Default-Scope, statt den uebergebenen Store-Code an ScopeConfigInterface weiterzureichen. Der Test besteht dann trotz eines echten Bugs, weil er nie einen zweiten Store simuliert.
4Wie teste ich den Fallback von Store- auf Website- oder Default-Scope?
Der ScopeConfigInterface-Mock liefert fuer den spezifischen Store-Scope null und nur auf Website- oder Default-Ebene einen Wert. So zeigt der Test, ob die Klasse den Fallback selbst korrekt behandelt oder faelschlich einen Wert erwartet.
5Brauche ich fuer Multi-Website-Tests auch StoreManagerInterface?
Ja, sobald Code den aktuellen Store dynamisch ermittelt statt ihn als Parameter zu erhalten. Ein gemockter StoreManagerInterface mit mehreren Store-Objekten und Website-Zuordnungen deckt das ab.
6Wie teste ich Shared-Customer-Account-Logik zwischen Websites?
Man mockt isSetFlag() fuer den Konfigurationspfad customer/account_share/scope und prueft beide Faelle: aktiviert und deaktiviert. So wird sichtbar, ob die Business-Logik korrekt websiteuebergreifend reagiert.
7Lohnt sich ein Data-Provider fuer Store-Tests?
Ja, besonders ab drei oder mehr Stores. Ein Data-Provider buendelt alle Store-Kombinationen in einer Methode und macht die Testdatei zugleich zu einer Uebersicht der erwarteten Store-Unterschiede.
8Wann ist ein echter Multi-Store-Integrationstest trotzdem noetig?
Wenn geprueft werden soll, ob Magento selbst die Scope-Aufloesung korrekt durchfuehrt, etwa bei komplexen Vererbungsketten oder Indexer-Logik, die store-spezifische Daten wirklich aus der Datenbank liest.
9Wie schnell sind Multi-Store-Unit-Tests mit Mocks im Vergleich zu echten Fixtures?
Deutlich schneller, da keine Datenbank involviert ist. Ein gemockter Test laeuft typischerweise in Millisekunden, waehrend ein Integrationstest mit echten Store-Fixtures durch Setup und Aufraeumarbeit spuerbar laenger dauert.
10Wie gehe ich mit @magentoConfigFixture fuer nicht-default Stores um?
Die Annotation erlaubt es, einen Konfigurationswert fuer einen bestimmten Store-Code direkt zu setzen, etwa @magentoConfigFixture fixturestore_store pfad wert. So laesst sich ein zweiter Store gezielt in einem Integrationstest abbilden.