EAV-Attribut-Aenderungen mit Tests absichern
AI generated
@test
assert
PHPUnit · Magento · EAV
EAV-Attribut-Aenderungen
mit PHPUnit-Tests absichern

Ein neues Attribut oder ein geaendertes Attribut-Set kann unbemerkt bestehende Produktdaten oder Frontend-Logik brechen. Wer Attribut-Setup-Skripte und die davon abhaengige Logik testet, findet solche Regressionen vor dem Deployment statt danach.

15 Min. Lesezeit EAV-Attribute Attribut-Sets Data-Patches

1. Warum EAV-Aenderungen ein besonderes Testrisiko sind

Das Entity-Attribute-Value-Modell macht Magento flexibel, aber auch fragil gegenueber unbedachten Aenderungen. Ein neues Attribut landet nicht in einer einzelnen Spalte, sondern in einer eigenen EAV-Tabelle, wird an ein oder mehrere Attribut-Sets angehaengt und kann je nach 'Scope' plötzlich store-spezifisch statt global gespeichert werden. Ein einziges falsch konfiguriertes Data-Patch kann dazu fuehren, dass ein Attribut, das vorher global war, ploetzlich pro Store-View unterschiedliche Werte annimmt, obwohl das nie beabsichtigt war.

Noch riskanter wird es, wenn ein bestehendes Attribut geaendert wird, etwa der Input-Typ von 'text' auf 'select' umgestellt wird. Bestehende Produktdaten, die als freier Text gespeichert wurden, passen dann nicht mehr zu den neuen Optionen und Frontend-Code, der den Wert direkt ausgibt, zeigt ploetzlich eine interne Options-ID statt eines lesbaren Labels. Ohne Tests faellt so etwas oft erst auf, wenn ein Kunde eine kaputte Produktseite meldet.

2. Ein Attribut-Setup-Skript selbst mit einem Integrationstest pruefen

Der direkteste Weg, ein Attribut-Setup-Skript abzusichern, ist ein Integrationstest, der das Data-Patch tatsaechlich ausfuehrt (oder dessen Effekt nach dem regulaeren Setup pruef) und anschliessend die resultierende Attribut-Definition ueber die ProductAttributeRepositoryInterface abfragt. So laesst sich verifizieren, dass das Attribut mit dem richtigen Code, dem richtigen Input-Typ und dem richtigen Scope tatsaechlich im System angekommen ist, nicht nur, dass das Setup-Skript ohne Fehler durchgelaufen ist.

Besonders wichtig ist die Pruefung des Scopes, weil ein falsch gesetzter Scope die haeufigste Ursache fuer spaetere Ueberraschungen ist. Ein Test, der explizit getIsGlobal() auf den erwarteten Wert prueft, deckt sofort auf, wenn ein Entwickler versehentlich 'Store View' statt 'Global' gewaehlt hat, ein Fehler, der im Admin-Panel leicht uebersehen wird, aber massive Auswirkungen auf die Datenkonsistenz hat.


<?php
declare(strict_types=1);

namespace Mironsoft\ProductAttributes\Test\Integration\Setup\Patch\Data;

use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;

class AddMaterialAttributeTest extends TestCase
{
    public function testMaterialAttributeIsCreatedWithExpectedProperties(): void
    {
        $objectManager = Bootstrap::getObjectManager();
        /** @var ProductAttributeRepositoryInterface $attributeRepository */
        $attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class);

        $attribute = $attributeRepository->get('material');

        self::assertSame('select', $attribute->getFrontendInput());
        self::assertSame(
            ScopedAttributeInterface::SCOPE_GLOBAL,
            (int) $attribute->getScope() !== null ? $attribute->getIsGlobal() : null
        );
        self::assertFalse((bool) $attribute->getIsRequired());
        self::assertTrue((bool) $attribute->getIsVisibleOnFront());
    }
}

3. Zuordnung zu Attribut-Sets und Gruppen absichern

Ein Attribut nuetzt wenig, wenn es zwar existiert, aber nicht den richtigen Attribut-Sets zugeordnet ist. Gerade in Shops mit vielen Produkttypen, etwa Bekleidung, Elektronik und Zubehoer mit jeweils eigenen Attribut-Sets, ist es leicht, beim Anlegen eines neuen Attributs eines der Sets zu vergessen. Ein Test, der die Zuordnung ueber AttributeSetRepositoryInterface und die zugehoerige Gruppe prueft, macht diese Luecke sofort sichtbar, statt sie erst zu entdecken, wenn ein Redakteur das Attribut im Admin-Panel vermisst.

Dabei lohnt es sich, nicht nur die Zugehoerigkeit zum Set zu pruefen, sondern auch, in welcher Attribut-Gruppe das Attribut landet. Ein Attribut, das versehentlich in der Gruppe 'Allgemein' statt in 'Technische Daten' landet, ist zwar technisch korrekt zugeordnet, sorgt aber im Redaktionsalltag fuer Verwirrung. Ein Test kann diese Erwartung explizit machen und damit auch dokumentieren, wie das Attribut-Set strukturiert sein soll.


<?php
declare(strict_types=1);

namespace Mironsoft\ProductAttributes\Test\Integration\Setup\Patch\Data;

use Magento\Catalog\Model\ResourceModel\Eav\Attribute as CatalogAttribute;
use Magento\Eav\Model\Config as EavConfig;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;

class MaterialAttributeSetAssignmentTest extends TestCase
{
    public function testMaterialIsAssignedToApparelAttributeSet(): void
    {
        $objectManager = Bootstrap::getObjectManager();
        /** @var EavConfig $eavConfig */
        $eavConfig = $objectManager->create(EavConfig::class);

        /** @var CatalogAttribute $attribute */
        $attribute = $eavConfig->getAttribute('catalog_product', 'material');
        $attributeSetInfo = $attribute->getAttributeSetInfo();

        $apparelSetId = $this->resolveAttributeSetId('Apparel');

        self::assertArrayHasKey($apparelSetId, $attributeSetInfo);
        self::assertSame('Technical Data', $attributeSetInfo[$apparelSetId]['group_id'] !== null
            ? $this->resolveGroupName($attributeSetInfo[$apparelSetId]['group_id'])
            : null);
    }

    private function resolveAttributeSetId(string $name): int
    {
        // Helper implementation resolves the attribute set id by name.
        return 4;
    }

    private function resolveGroupName(int $groupId): string
    {
        // Helper implementation resolves the group name by id.
        return 'Technical Data';
    }
}

4. Bestehende Produktdaten vor destruktiven Attribut-Aenderungen schuetzen

Die riskanteste Kategorie von Aenderungen ist nicht das Anlegen neuer Attribute, sondern das Aendern bestehender. Ein Input-Typ-Wechsel von 'text' zu 'select', ein geloeschter Options-Wert oder eine Aenderung von 'multiselect' zu 'select' kann bestehende Produktdaten unbrauchbar machen. Ein guter Test simuliert genau diesen Fall: Er legt zunaechst ein Produkt mit dem alten Attributwert an, fuehrt dann die Aenderung aus (oder prueft deren bereits angewendeten Zustand) und verifiziert, dass die alten Daten entweder korrekt migriert wurden oder zumindest nicht stillschweigend verloren gehen.

Besonders wichtig ist das bei Data-Patches, die Optionswerte umbenennen oder loeschen. Ein Test, der vor der Aenderung ein Produkt mit dem betroffenen Options-Wert anlegt und nach der Aenderung prueft, ob der Produktwert noch aufloesbar ist, deckt genau die Faelle ab, in denen ein Redakteur beim Aufraeumen von Attribut-Optionen versehentlich noch genutzte Werte entfernt.


<?php
declare(strict_types=1);

namespace Mironsoft\ProductAttributes\Test\Integration\Setup\Patch\Data;

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

/**
 * @magentoDataFixture Magento/Catalog/_files/product_simple.php
 */
class MaterialOptionMigrationTest extends TestCase
{
    public function testExistingProductKeepsResolvableMaterialValue(): void
    {
        $objectManager = Bootstrap::getObjectManager();
        /** @var ProductRepositoryInterface $productRepository */
        $productRepository = $objectManager->create(ProductRepositoryInterface::class);

        $product = $productRepository->get('simple');
        $materialValue = $product->getData('material');

        self::assertNotNull($materialValue, 'Material value must not have been silently wiped.');
        self::assertNotSame('', (string) $materialValue);
    }
}

5. Frontend-Logik, die auf Attributwerte zugreift, isoliert testen

Neben dem Setup selbst lohnt es sich, jede Klasse zu testen, die einen bestimmten EAV-Attributwert liest und daraus Anzeige- oder Business-Logik ableitet, etwa ein ViewModel, das ein 'material'-Attribut in einen lesbaren Badge-Text uebersetzt. Ein Unit-Test mit einem gemockten ProductInterface deckt ab, wie sich die Klasse verhaelt, wenn der Attributwert vorhanden, leer oder unerwartet ist, etwa eine numerische Options-ID statt eines Textlabels.

Gerade der Fall eines leeren oder null-Attributwerts wird in der Praxis oft vergessen, ist aber real: Nicht jedes Produkt hat zwingend jedes Attribut befuellt, besonders nach einer Attribut-Set-Aenderung, wenn bestehende Produkte das neue Attribut noch nicht gepflegt bekommen haben. Ein Test, der explizit einen leeren Wert simuliert und einen sinnvollen Fallback erwartet, verhindert, dass im Frontend ploetzlich 'N/A' oder ein leerer Badge auftaucht.


<?php
declare(strict_types=1);

namespace Mironsoft\ProductAttributes\Test\Unit\ViewModel;

use Magento\Catalog\Api\Data\ProductInterface;
use Mironsoft\ProductAttributes\ViewModel\MaterialBadge;
use PHPUnit\Framework\TestCase;

class MaterialBadgeTest extends TestCase
{
    public function testReturnsReadableLabelWhenMaterialIsSet(): void
    {
        $productMock = $this->createMock(ProductInterface::class);
        $productMock->method('getData')->with('material')->willReturn('cotton');

        $viewModel = new MaterialBadge();

        self::assertSame('Cotton', $viewModel->getBadgeText($productMock));
    }

    public function testReturnsFallbackWhenMaterialIsEmpty(): void
    {
        $productMock = $this->createMock(ProductInterface::class);
        $productMock->method('getData')->with('material')->willReturn(null);

        $viewModel = new MaterialBadge();

        self::assertSame('', $viewModel->getBadgeText($productMock));
    }
}

6. Attribut-Set-Struktur als Ganzes gegen eine erwartete Referenz pruefen

Bei groesseren Shops mit vielen Attribut-Sets lohnt sich ein Test, der die komplette Struktur eines Attribut-Sets, also alle zugeordneten Attribute samt Gruppen, gegen eine erwartete Referenzliste vergleicht. Das faengt Faelle ab, in denen ein Entwickler beim Refactoring versehentlich ein bestehendes Attribut aus einem Set entfernt, ohne das zu beabsichtigen, etwa durch ein fehlerhaftes Merge im Data-Patch.

Ein solcher Struktur-Test ist bewusst grob gehalten: Er prueft nicht jedes Detail, sondern vergleicht die Menge der Attribut-Codes im Set gegen eine erwartete Menge. Das macht ihn robust gegen kleine, gewollte Aenderungen an einzelnen Attributen, aber empfindlich gegen versehentliches Hinzufuegen oder Entfernen ganzer Attribute, was in der Praxis der weitaus haeufigere und schaedlichere Fehler ist.


<?php
declare(strict_types=1);

namespace Mironsoft\ProductAttributes\Test\Integration\Setup\Patch\Data;

use Magento\Eav\Model\Config as EavConfig;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;

class ApparelAttributeSetStructureTest extends TestCase
{
    public function testApparelAttributeSetContainsExpectedAttributes(): void
    {
        $objectManager = Bootstrap::getObjectManager();
        /** @var EavConfig $eavConfig */
        $eavConfig = $objectManager->create(EavConfig::class);

        $expectedAttributeCodes = [
            'name', 'sku', 'price', 'material', 'size', 'color', 'season',
        ];

        $entityType = $eavConfig->getEntityType('catalog_product');
        $actualAttributeCodes = array_map(
            static fn ($attribute) => $attribute->getAttributeCode(),
            $eavConfig->getEntityAttributes($entityType, null)
        );

        foreach ($expectedAttributeCodes as $code) {
            self::assertContains($code, $actualAttributeCodes, "Missing expected attribute: {$code}");
        }
    }
}

7. Auswirkungen auf Indexer und Suchfilter mit einbeziehen

EAV-Attribute mit aktivierter Filterfunktion im Layered Navigation oder mit Suchindex-Relevanz haben eine Wirkung, die ueber das reine Produktdatenmodell hinausgeht. Wird ein Attribut von 'Nutzen fuer Layered Navigation: Ja' auf 'Nein' umgestellt, verschwindet es aus dem Filter, aber bestehender Code, der explizit nach diesem Filter sucht, findet ihn ploetzlich nicht mehr. Ein Test sollte deshalb auch pruefen, ob die relevanten Indexer-Flags des Attributs, etwa getIsFilterable() und getIsSearchable(), dem erwarteten Zustand entsprechen.

Diese Pruefung ist bewusst von einem echten Reindex-Lauf getrennt zu halten. Es geht hier nicht darum, ob der Indexer korrekt arbeitet, sondern darum, ob die Attribut-Konfiguration selbst die richtigen Flags setzt, die der Indexer dann als Eingabe verwendet. Ein separater Test fuer die eigentliche Indexer-Logik ist ein eigenes Thema mit eigenen Werkzeugen, siehe dazu den Artikel ueber isoliertes Testen von Indexer-Klassen.

8. Data-Patches ruckwaertskompatibel und idempotent testen

Ein oft uebersehener Aspekt von Attribut-Setup-Skripten ist Idempotenz: Ein Data-Patch kann in manchen Deployment-Szenarien mehrfach ausgefuehrt werden, etwa wenn ein Modul in unterschiedlichen Umgebungen mit leicht abweichendem Ausfuehrungsstand installiert wird. Ein Patch, der beim zweiten Lauf einen Fehler wirft, weil er versucht, ein bereits existierendes Attribut erneut anzulegen, kann ein komplettes Deployment blockieren.

Ein Test, der das Patch-Objekt zweimal hintereinander ausfuehrt und erwartet, dass beim zweiten Lauf kein Fehler auftritt, deckt genau dieses Risiko ab. In der Praxis bedeutet das meist, dass das Setup-Skript vor dem Anlegen eines Attributs prueft, ob es bereits existiert, statt blind auf addAttribute() zu vertrauen. Dieser Test ist gerade bei komplexen Multi-Modul-Deployments mit mehreren Abhaengigkeiten ein wichtiges Sicherheitsnetz.


<?php
declare(strict_types=1);

namespace Mironsoft\ProductAttributes\Test\Integration\Setup\Patch\Data;

use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\TestFramework\Helper\Bootstrap;
use Mironsoft\ProductAttributes\Setup\Patch\Data\AddMaterialAttribute;
use PHPUnit\Framework\TestCase;

class AddMaterialAttributeIdempotencyTest extends TestCase
{
    public function testPatchCanBeAppliedTwiceWithoutError(): void
    {
        $objectManager = Bootstrap::getObjectManager();
        /** @var AddMaterialAttribute $patch */
        $patch = $objectManager->create(AddMaterialAttribute::class);

        self::assertInstanceOf(DataPatchInterface::class, $patch);

        $patch->apply();
        $patch->apply();

        self::assertTrue(true, 'No exception was thrown on the second apply() call.');
    }
}

9. Eine wiederverwendbare Checkliste fuer Attribut-Aenderungen

Aus den vorherigen Beispielen laesst sich eine wiederverwendbare Checkliste ableiten, die sich fuer jedes neue Attribut-Setup-Skript eignet: Existiert das Attribut mit dem korrekten Input-Typ und Scope, ist es den erwarteten Attribut-Sets und Gruppen zugeordnet, bleiben bestehende Produktdaten bei Aenderungen erhalten oder werden korrekt migriert, verhaelt sich abhaengige Frontend-Logik korrekt bei leeren Werten, und ist das Patch idempotent.

Diese Checkliste laesst sich direkt in eine Testklasse pro Attribut-Setup-Skript uebersetzen, mit einer Testmethode pro Punkt. Das Ergebnis ist eine Testklasse, die nicht nur technische Korrektheit prueft, sondern gleichzeitig als lesbare Dokumentation dient, was das Team beim Anlegen dieses Attributs bewusst entschieden hat, ein Wert, der weit ueber die reine Fehlervermeidung hinausgeht.

Was pruefen Testebene Typischer Fehler ohne Test Empfohlene Assertion
Attribut existiert mit korrektem Scope Integrationstest Scope faelschlich auf Store View statt Global assertSame() auf getIsGlobal()
Attribut-Set-Zuordnung Integrationstest Attribut fehlt in einem der Produkttyp-Sets assertArrayHasKey() im Attribute-Set-Info-Array
Bestehende Produktdaten bleiben nutzbar Integrationstest mit Fixture Options-Wert nach Umbenennung nicht mehr aufloesbar assertNotNull() auf den migrierten Wert
Frontend-Logik bei leerem Attributwert Unit-Test mit gemocktem Produkt Leeres Attribut fuehrt zu kaputtem Badge/Layout assertSame() auf erwarteten Fallback-Text
Idempotenz des Data-Patches Integrationstest, Patch zweimal ausgefuehrt Zweiter Lauf wirft Exception und blockiert Deployment Kein Fehler beim zweiten apply()-Aufruf

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

EAV-Attribut-Tests: Das Wichtigste auf einen Blick

Kernidee

Attribut-Setup, Attribut-Set-Zuordnung, bestehende Produktdaten und Frontend-Fallbacks gezielt testen.

Groesstes Risiko

Input-Typ- oder Scope-Aenderungen an bestehenden Attributen brechen unbemerkt Altdaten.

Werkzeug

Integrationstests mit ProductAttributeRepositoryInterface und EavConfig fuer die reale Struktur.

Zusatz-Absicherung

Idempotenz-Test stellt sicher, dass Data-Patches ein zweites Deployment nicht blockieren.

11. FAQ: EAV-Attribut-Tests: Das Wichtigste auf einen Blick

1Wie teste ich, ob ein neu angelegtes EAV-Attribut den richtigen Scope hat?
Mit einem Integrationstest, der das Attribut ueber ProductAttributeRepositoryInterface laedt und getIsGlobal() beziehungsweise den Scope-Wert gegen den erwarteten Wert prueft.
2Reicht ein Unit-Test, um ein Attribut-Setup-Skript abzusichern?
Nein, das Setup-Skript selbst braucht einen Integrationstest, weil es tatsaechlich mit der EAV-Struktur interagiert. Unit-Tests eignen sich fuer Klassen, die spaeter mit dem Attributwert arbeiten.
3Wie stelle ich sicher, dass ein Attribut im richtigen Attribut-Set landet?
Ueber einen Integrationstest, der die Attribut-Set-Info des Attributs oder die Struktur des Attribut-Sets selbst gegen eine erwartete Liste von Attribut-Codes prueft.
4Wie teste ich, ob bestehende Produktdaten eine Attribut-Aenderung ueberleben?
Mit einer Produkt-Fixture, die vor der Aenderung existiert, gefolgt von einer Pruefung, ob der Attributwert nach der Aenderung noch korrekt aufloesbar ist und nicht stillschweigend verloren ging.
5Was ist der haeufigste Fehler bei Aenderungen an bestehenden Attributen?
Ein Wechsel des Input-Typs, etwa von text zu select, ohne bestehende Werte zu migrieren. Frontend-Code zeigt dann eine interne Options-ID statt eines lesbaren Labels.
6Wie teste ich Frontend-Logik, die auf ein moeglicherweise leeres Attribut zugreift?
Mit einem Unit-Test, der ein gemocktes Produkt mit null oder leerem Attributwert uebergibt und prueft, ob die Klasse einen sinnvollen Fallback statt eines kaputten Layouts liefert.
7Warum ist Idempotenz bei Data-Patches wichtig?
Weil ein Patch in manchen Deployment-Szenarien mehrfach ausgefuehrt werden kann. Ein Patch, der beim zweiten Lauf einen Fehler wirft, kann ein komplettes Deployment blockieren.
8Sollte ich die komplette Attribut-Set-Struktur in einem Test abbilden?
Ein grober Struktur-Test, der die Menge der Attribut-Codes im Set prueft, ist sinnvoll und robust. Er faengt versehentliches Entfernen von Attributen ab, ohne bei jeder kleinen Detailaenderung fehlzuschlagen.
9Muss ich Indexer-Flags wie getIsFilterable() separat testen?
Ja, als Teil der Attribut-Konfigurationspruefung. Der eigentliche Indexer-Lauf ist ein separates Testthema mit eigenen, isolierten Tests der Indexer-Logik-Klassen.
10Wie oft sollten Attribut-Tests in der CI-Pipeline laufen?
Integrationstests fuer Attribut-Setup gehoeren in die Standard-Testphase, da sie fuer die Datenintegritaet kritisch sind. Sie sollten bei jedem Merge-Request laufen, nicht nur vor Releases.