@test
assert
Magento · PHPUnit · SearchCriteria · EAV · Collections
SearchCriteria, Collections
und EAV-Zugriffe in Tests absichern

SearchCriteria-Abfragen, Collection-Filter und EAV-Attribute sind die drei häufigsten Datenzugriffsmuster in Magento – und gleichzeitig die drei häufigsten Stellen, an denen Tests entweder fehlen oder falsch strukturiert sind. Dieser Artikel zeigt, welcher Testtyp für welchen Zugriff geeignet ist und wie man jeden korrekt absichert.

17 Min. Lesezeit SearchCriteria · Collections · EAV · Repository · Unit + Integration Magento 2.4.x · PHPUnit 10/11 · PHP 8.4

1. Welcher Test für welchen Datenzugriff?

Magento bietet zwei Hauptwege für den Datenzugriff: das Repository-Pattern mit SearchCriteria (Service Contracts, empfohlener Weg seit Magento 2.1) und direkte Collection-Abfragen (älteres Muster, aber weiterhin verbreitet, besonders im Adminbereich). EAV-Zugriffe (Entity-Attribute-Value) sind ein Querschnittsthema, das sowohl über Repositories als auch direkt über Collections erfolgen kann. Die Wahl der Teststrategie hängt von diesem Zugriffsmuster ab.

Für Services und ViewModels, die Repositories über Interfaces injiziert bekommen, sind Unit-Tests mit Repository-Mocks die richtige Wahl: schnell, keine Datenbankverbindung, vollständige Kontrolle über Rückgabewerte. Für die Repositories selbst und für Collection-Abfragen, die SQL-Joins oder EAV-spezifische Joins enthalten, sind Integrationstests mit echtem Magento-Bootstrap nötig. Ein häufiger Fehler in Magento-Projekten: Unit-Tests für Collection-Logik schreiben, die implizit von echten EAV-Joins abhängen, und sich dann wundern, warum das Verhalten im Produktionsbetrieb vom Test abweicht.

2. SearchCriteria in Unit-Tests: Repository mocken

Wenn ein Service oder ViewModel über ein Repository-Interface Daten abfragt, ist das Repository der externe Abhängigkeitspunkt. In Unit-Tests wird dieses Repository gemockt. Der Mock muss konfiguriert werden, um auf spezifische SearchCriteria-Aufrufe mit vordefinierten Ergebnissen zu antworten. Das korrekte Muster: den SearchCriteriaInterface-Mock als Return-Wert für SearchCriteriaBuilder::create() konfigurieren und das Repository so einrichten, dass es bei jedem getList()-Aufruf ein vorbereitetes SearchResultsInterface zurückgibt.

Ein häufiger Fehler ist es, den SearchCriteriaBuilder in Unit-Tests real zu instanziieren, was Magento-Framework-Abhängigkeiten zieht. Stattdessen sollte der Builder entweder gemockt oder durch ein TestDouble ersetzt werden, das SearchCriteriaInterface-Instanzen zurückgibt. Da der Unit-Test nicht die Korrektheit der SQL-Abfrage prüft, sondern die Logik des Services, die das Ergebnis verarbeitet, ist es korrekt, das Repository-Mock immer eine vordefinierte Ergebnisliste zurückgeben zu lassen – unabhängig vom übergebenen SearchCriteria-Objekt.


<?php

declare(strict_types=1);

namespace Mironsoft\Catalog\Test\Unit\Model;

use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Api\SearchResultsInterface;
use Mironsoft\Catalog\Api\ProductRepositoryInterface;
use Mironsoft\Catalog\Model\ActiveProductService;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

/**
 * Unit test for ActiveProductService.
 * Repository is mocked — no database, no Magento bootstrap.
 * Tests the service logic, not the SearchCriteria SQL.
 */
#[CoversClass(ActiveProductService::class)]
final class ActiveProductServiceTest extends TestCase
{
    private MockObject&ProductRepositoryInterface $repositoryMock;
    private ActiveProductService $service;

    protected function setUp(): void
    {
        $this->repositoryMock = $this->createMock(ProductRepositoryInterface::class);
        $searchCriteriaBuilderMock = $this->createMock(
            \Magento\Framework\Api\SearchCriteriaBuilder::class
        );

        // Builder returns a SearchCriteria mock — we don't care about its content in unit tests
        $searchCriteriaMock = $this->createMock(SearchCriteriaInterface::class);
        $searchCriteriaBuilderMock->method('addFilter')->willReturnSelf();
        $searchCriteriaBuilderMock->method('create')->willReturn($searchCriteriaMock);

        $this->service = new ActiveProductService(
            repository: $this->repositoryMock,
            searchCriteriaBuilder: $searchCriteriaBuilderMock
        );
    }

    #[Test]
    public function testGetActiveProducts_WhenRepositoryReturnsItems_ReturnsFilteredList(): void
    {
        $productMock = $this->createConfiguredMock(
            \Mironsoft\Catalog\Api\Data\ProductInterface::class,
            ['getId' => 1, 'getSku' => 'ACTIVE-001', 'getStatus' => 1]
        );

        $searchResultsMock = $this->createConfiguredMock(
            SearchResultsInterface::class,
            ['getItems' => [$productMock], 'getTotalCount' => 1]
        );

        // Repository always returns our prepared results — regardless of SearchCriteria
        $this->repositoryMock
            ->expects($this->once())
            ->method('getList')
            ->willReturn($searchResultsMock);

        $result = $this->service->getActiveProducts();

        self::assertCount(1, $result);
        self::assertSame('ACTIVE-001', $result[0]->getSku());
    }
}

3. SearchCriteria in Integrationstests: echte Datenbank

Die Korrektheit einer SearchCriteria-Abfrage – dass die Filterkriterien tatsächlich die richtigen Datenbankzeilen liefern – kann nur ein Integrationstest mit echter Datenbank prüfen. Magento-Integrationstests verwenden dazu den offiziellen Test-Bootstrap unter dev/tests/integration, der das vollständige Magento-Framework mit DI-Container, echten Repositories und einer dedizierten Testdatenbank startet. Der ObjectManager ist im Integrationstest verfügbar und instanziiert alle Objekte über das echte DI-System.

Integrationstests in Magento erben typischerweise von Magento\TestFramework\TestCase\AbstractController für Controller-Tests oder direkt von PHPUnit\Framework\TestCase mit Magento-Annotationen für Service-Tests. Das wichtigste Merkmal: @magentoDbIsolation enabled (Standard) sorgt dafür, dass alle Datenbankänderungen nach jedem Test zurückgerollt werden. Dadurch können Tests Testdaten in die Datenbank schreiben, ohne sich um Cleanup kümmern zu müssen.


<?php

declare(strict_types=1);

namespace Mironsoft\Catalog\Test\Integration\Model;

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

/**
 * Integration test for SearchCriteria-based product queries.
 * Uses real Magento bootstrap and database.
 * Run with: bin/magento dev:tests:run integration
 *
 * @magentoDbIsolation enabled
 * @magentoDataFixture Mironsoft_Catalog::Test/Integration/_files/active_products.php
 */
final class ProductSearchIntegrationTest extends TestCase
{
    private ProductRepositoryInterface $productRepository;
    private SearchCriteriaBuilder $searchCriteriaBuilder;

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

    #[Test]
    public function testGetList_WithStatusFilter_ReturnsOnlyActiveProducts(): void
    {
        $searchCriteria = $this->searchCriteriaBuilder
            ->addFilter('status', 1, 'eq')
            ->addFilter('visibility', [2, 3, 4], 'in')
            ->create();

        $result = $this->productRepository->getList($searchCriteria);

        // Fixture creates 3 active + 2 disabled products
        self::assertGreaterThanOrEqual(3, $result->getTotalCount());

        foreach ($result->getItems() as $product) {
            self::assertEquals(1, $product->getStatus(), "Product {$product->getSku()} must be active");
        }
    }
}

4. Collections: Filter und Joins in Tests absichern

Magento-Collections sind eine ältere Abstraktionsschicht über Zend_Db-Abfragen. Sie bieten addFieldToFilter(), addAttributeToFilter() (für EAV) und direkte getSelect()->join()-Aufrufe. Das Hauptproblem für Tests: Collections haben starke implizite Abhängigkeiten von der Datenbankverbindung und dem EAV-System. Ein ProductCollection->load() ohne Datenbankverbindung schlägt immer fehl. Daher sind Collection-Tests fast immer Integrationstests.

Für Unit-Tests von Code, der Collections konsumiert (nicht erzeugt), gibt es einen anderen Ansatz: den Service oder das ViewModel durch ein Repository-Interface abstrahieren und das Repository mocken. Der Collection-Code selbst wird dann in einem Integrationstest abgesichert, der echte Daten verwendet. Diese Trennung – Unit-Test für Konsumenten-Logik, Integrationstest für Collection-Abfrage – ist das empfohlene Muster und vermeidet die Komplexität, Collections zu mocken.

5. EAV-Zugriffe: Attribute laden und testen

Das EAV-System (Entity-Attribute-Value) ist eines der komplexesten Subsysteme von Magento. Produkt-Attribute, Kategorie-Attribute und Kunden-Attribute werden über separate Tabellen mit dynamischen Joins geladen. Diese Joins sind datenbankspezifisch und können in Unit-Tests nicht sinnvoll simuliert werden. EAV-Zugriffe müssen daher in Integrationstests abgesichert werden.

Für Code, der EAV-Attribute liest (z.B. ein ViewModel, das $product->getData('custom_attribute') aufruft), gibt es trotzdem eine Unit-Test-Strategie: das Produkt-Objekt wird als Mock mit vordefinierten getData()-Rückgabewerten übergeben. Das EAV-System dahinter wird nicht getestet – das ist in Ordnung, weil das Ziel des Unit-Tests die Verarbeitungslogik des ViewModels ist, nicht das EAV-System selbst. Der Integrationstest stellt dann sicher, dass das EAV-Attribut tatsächlich geladen wird und die korrekten Werte zurückgibt.


<?php

declare(strict_types=1);

namespace Mironsoft\Catalog\Test\Unit\ViewModel;

use Magento\Catalog\Api\Data\ProductInterface;
use Mironsoft\Catalog\ViewModel\ProductSpecificationViewModel;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

/**
 * Unit test for EAV-consuming ViewModel.
 * Product is mocked — EAV system not involved.
 * Tests: how the ViewModel processes EAV attribute values.
 *
 * Integration test (separate file) verifies the EAV attribute itself loads correctly.
 */
#[CoversClass(ProductSpecificationViewModel::class)]
final class ProductSpecificationViewModelTest extends TestCase
{
    private ProductSpecificationViewModel $viewModel;

    protected function setUp(): void
    {
        $this->viewModel = new ProductSpecificationViewModel();
    }

    #[Test]
    public function testGetMaterial_WhenEavAttributeSet_ReturnsFormattedValue(): void
    {
        // Mock product with EAV attribute value pre-set
        $productMock = $this->createConfiguredMock(
            ProductInterface::class,
            // getData() returns the raw EAV value — EAV loading is not tested here
            ['getData' => 'cotton_organic']
        );

        $result = $this->viewModel->getMaterial($productMock);

        // ViewModel formats the raw value: 'cotton_organic' → 'Cotton (Organic)'
        self::assertSame('Cotton (Organic)', $result);
    }

    #[Test]
    public function testGetMaterial_WhenEavAttributeNotSet_ReturnsEmptyString(): void
    {
        $productMock = $this->createConfiguredMock(
            ProductInterface::class,
            ['getData' => null]
        );

        $result = $this->viewModel->getMaterial($productMock);

        self::assertSame('', $result);
    }
}

6. Test-Fixtures für Magento-Integrationstests

Test-Fixtures in Magento-Integrationstests sind PHP-Dateien, die über die Annotation @magentoDataFixture eingebunden werden. Sie laufen vor dem Test und erstellen Testdaten in der Datenbank. Durch @magentoDbIsolation enabled werden alle Datenbankänderungen nach dem Test zurückgerollt. Fixture-Dateien sollten minimal sein: nur die Daten anlegen, die der Test wirklich benötigt, und keine Abhängigkeiten zu anderen Fixtures haben. Gut strukturierte Fixtures sind eigenständig, idempotent (können mehrfach ausgeführt werden, ohne Konflikte zu verursachen) und produzieren stabile, vorhersagbare Testdaten.

7. Teststrategien für Magento-Datenzugriff im Vergleich

Die Wahl der richtigen Teststrategie für Magento-Datenzugriffsmuster hängt von der Frage ab: Was soll der Test prüfen? Die Service-Logik, die Repository-Ergebnisse verarbeitet, oder die Korrektheit der Datenbankabfrage selbst? Diese zwei Fragen führen zu zwei verschiedenen Testtypen.

Datenzugriff Unit-Test Integrationstest Was wird geprüft
Repository via Interface Repository mocken Optional Service-Logik, nicht SQL
SearchCriteria-Filter Nicht sinnvoll Echter Datenbankzugriff Korrektheit der SQL-Filter
Collection mit Joins Nicht sinnvoll Echter Datenbankzugriff Join-Korrektheit, Filterergebnisse
EAV-Attribut laden Produkt-Mock mit getData() Echtes EAV, echte DB Attributwerte und EAV-Join
EAV-Wert verarbeiten Produkt-Mock verwenden Optional Verarbeitungslogik im ViewModel

Der häufigste Fehler in Magento-Projekten: Integrationstests für Service-Logik schreiben, obwohl Unit-Tests mit Repository-Mocks ausreichen würden. Das treibt die Testlaufzeit in die Höhe und macht Tests von der Datenbankzustand abhängig. Die zweithäufigste Variante: Unit-Tests für Collection-Logik schreiben, was zu Mocks führt, die die echte Collection-API schlecht abbilden und Vertrauen in Tests suggerieren, das nicht gerechtfertigt ist.

Mironsoft

Magento-Testarchitektur, SearchCriteria, EAV und Integration-Test-Setup

Magento-Datenzugriffe testbar machen?

Wir analysieren eure Repositories, Collections und EAV-Zugriffe, wählen die passende Teststrategie für jeden Zugriffstyp und implementieren Unit- und Integrationstests mit den korrekten Fixtures für euer Magento-Projekt.

Test-Audit

Bestehende Magento-Tests analysieren und falsch strukturierte Tests identifizieren

Strategie-Beratung

Unit vs. Integration für jeden Datenzugriffstyp definieren, Testpyramide aufbauen

Implementierung

Tests, Fixtures und Integration-Test-Bootstrap für euer Magento-Projekt einrichten

8. Zusammenfassung

Die korrekte Teststrategie für Magento-Datenzugriffe folgt einer klaren Regel: Was testen wir? Wenn wir die Service-Logik testen, die Repository-Ergebnisse verarbeitet, sind Unit-Tests mit Repository-Mocks der richtige Weg. Wenn wir die Korrektheit einer SearchCriteria-Abfrage, eines Collection-Filters oder eines EAV-Joins testen, brauchen wir einen Integrationstest mit echter Datenbank.

Das häufigste Symptom eines falsch strukturierten Magento-Test-Setups: Integrationstests für alles, weil "man nie sicher sein kann, wie Magento intern arbeitet". Das Ergebnis sind langsame, fragile Test-Suiten, die bei jedem Datenbankzustandsproblem fehlschlagen. Die richtige Antwort ist nicht mehr Integrationstests, sondern bessere Abstraktion: Code, der über Repository-Interfaces arbeitet, kann vollständig in Unit-Tests abgesichert werden. Nur die Implementierung der Repositories selbst – und Collection-Abfragen mit Datenbanklogik – braucht Integrationstests.

SearchCriteria, Collections und EAV — Das Wichtigste auf einen Blick

Repository-Interface

Repository in Unit-Tests mocken. Service-Logik prüfen, nicht SQL. Das Repository selbst braucht einen Integrationstest.

SearchCriteria-Filter

Nur im Integrationstest prüfbar. Fixtures anlegen, echte Filter ausführen, Ergebnisse gegen erwartete Datensätze prüfen.

EAV-Attribute

EAV-Laden: Integrationstest. EAV-Werte verarbeiten: Unit-Test mit Produkt-Mock und vordefinierten getData()-Rückgabewerten.

Fixtures

@magentoDataFixture für Integrationstests. Minimal und eigenständig. @magentoDbIsolation rollback räumt automatisch auf.

9. FAQ: SearchCriteria, Collections und EAV in Tests

1SearchCriteria in Unit-Tests testen?
Die SQL-Filter-Korrektheit: nur im Integrationstest. Die Service-Logik, die Repository-Ergebnisse verarbeitet: Unit-Test mit Repository-Mock.
2SearchCriteriaBuilder mocken?
addFilter()->willReturnSelf(), create() gibt SearchCriteriaMock zurück. Repository-Mock gibt bei jedem getList()-Aufruf vordefiniertes SearchResults zurück.
3Warum Collections immer Integrationstests?
Starke DB-Abhängigkeiten durch SQL-Generierung, EAV-Joins und Resource-Model. Nicht sinnvoll durch Mocks simulierbar.
4EAV-Attribute in Unit-Tests?
Produkt-Mock mit getData('attribute_code') = Testwert konfigurieren. So wird die Verarbeitungslogik getestet, ohne das EAV-System.
5Was ist @magentoDbIsolation?
Umschließt jeden Integrationstest in einer Datenbanktransaktion, die am Ende zurückgerollt wird. Kein manuelles Cleanup nötig. Standard: enabled.
6Test-Fixtures für Magento strukturieren?
Minimal, eigenständig, über echte Magento-APIs anlegen (nicht SQL). Dateiname spiegelt Inhalt: active_products.php.
7Muss jedes Repository einen Integrationstest haben?
Standard-CRUD-Delegation: kein eigener Test nötig. Benutzerdefinierte Joins, Filter, Aggregation: Integrationstest empfohlen.
8Integrationstests in Docker ausführen?
bin/magento dev:tests:run integration oder vendor/bin/phpunit dev/tests/integration/phpunit.xml im PHP-Container. Dedizierte Testdatenbank über install-config-mysql.php konfigurieren.
9EAV-Collections mocken sinnvoll?
Nein – komplexe interne EAV-Zustände lassen sich nicht sinnvoll simulieren. Integrationstest ist einfacher zu schreiben und zuverlässiger.
10Custom EAV-Attribute testen?
Im Integrationstest: Attribut anlegen, Produkt mit Attributwert speichern, laden und Wert prüfen. Stellt sicher, dass EAV-Schema, Storage und Loading zusammenarbeiten.