Produktive PHPUnit-Checkliste: Struktur, Geschwindigkeit, Docker, PhpStorm, Magento
AI generated
@test
assert
PHPUnit · Checkliste · Docker · Magento · PhpStorm
Produktive PHPUnit-Checkliste
Struktur, Geschwindigkeit, Docker, PhpStorm & Magento

Eine langsame, unstrukturierte Test-Suite ist keine Test-Suite – sie ist ein Bremsklotz. Diese Checkliste deckt alle Stellen auf, an denen PHPUnit-Setups Zeit, Wartbarkeit und Entwicklervertrauen verlieren, und gibt zu jedem Punkt eine direkt umsetzbare Maßnahme.

18 Min. Lesezeit Struktur · Laufzeit · Docker · PhpStorm · Magento PHPUnit 10/11 · PHP 8.3/8.4 · Magento 2.4.x

1. Was eine produktive Test-Suite ausmacht

Produktiv ist eine Test-Suite dann, wenn sie schnell genug läuft, um im täglichen Entwicklungszyklus verwendet zu werden, wenn ihre Ausgaben klar genug sind, um Fehler ohne Debugging zu verstehen, und wenn ihre Struktur klar genug ist, um neue Tests ohne langen Einarbeitungsaufwand hinzuzufügen. Diese drei Kriterien werden in den meisten PHP-Projekten nicht gleichzeitig erfüllt. Häufig ist eine der drei Dimensionen vernachlässigt: entweder sind Tests zu langsam, oder ihre Fehlermeldungen sind kryptisch, oder die Struktur ist so organisch gewachsen, dass niemand mehr weiß, wo ein neuer Test hingehört.

Die folgende Checkliste geht systematisch durch alle Bereiche, die eine produktive PHPUnit-Suite ausmachen. Jeder Punkt ist direkt umsetzbar – mit einem klaren Ja/Nein-Kriterium und einer Maßnahme, wenn das Kriterium nicht erfüllt ist. Die Checkliste ist auf PHP-Projekte mit Magento-Anteil ausgelegt, aber alle Punkte zu Struktur, Laufzeit und Docker sind auf jedes PHP-Projekt übertragbar.

2. Checkliste: Teststruktur und Namenskonventionen

Eine konsistente Teststruktur ist die Voraussetzung dafür, dass das gesamte Team ohne Absprache Tests findet, liest und ergänzt. Die Verzeichnisstruktur spiegelt idealerweise die Struktur des Produktionscodes: jede Klasse in src/Model/ProductEnricher.php hat ihre Testklasse in Test/Unit/Model/ProductEnricherTest.php. Der Suffix Test ist PHPUnit-Konvention und sollte nicht variiert werden. Integrationstests und Unit-Tests müssen in getrennten Verzeichnissen liegen und über separate Test-Suiten in der phpunit.xml adressierbar sein.

Testmethodennamen sind dokumentation. Ein Methodenname wie testWorks erklärt nichts; testReturnsZeroWhenCartIsEmpty erklärt Precondition, Action und Expected Outcome. Das Given/When/Then-Muster oder der test[SUT]_[Action]_[ExpectedBehavior]-Stil sind beide akzeptable Konventionen – wichtig ist, dass das gesamte Team dieselbe Konvention verwendet. Fehlende oder inkonsistente Namenskonventionen sind der häufigste Grund dafür, dass neue Entwickler eine Test-Suite nicht verstehen und deshalb keine neuen Tests schreiben.


<?php

declare(strict_types=1);

namespace Mironsoft\Catalog\Test\Unit\Model;

use Mironsoft\Catalog\Model\PriceCalculator;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

/**
 * Unit tests for PriceCalculator.
 *
 * Naming convention: test[What]_[Condition]_[ExpectedResult]
 * Each test has exactly one logical assertion.
 * Test class mirrors production class path: Model/PriceCalculator → Test/Unit/Model/PriceCalculatorTest
 */
#[CoversClass(PriceCalculator::class)]
final class PriceCalculatorTest extends TestCase
{
    private PriceCalculator $calculator;

    protected function setUp(): void
    {
        // No mocks needed — PriceCalculator has no external dependencies
        $this->calculator = new PriceCalculator(taxRate: 0.19);
    }

    #[Test]
    public function testCalculateGross_WithNetPrice_ReturnsNetPlusVat(): void
    {
        $gross = $this->calculator->calculateGross(net: 100.00);

        self::assertEqualsWithDelta(119.00, $gross, 0.001);
    }

    #[Test]
    #[DataProvider('negativePriceProvider')]
    public function testCalculateGross_WithNegativePrice_ThrowsInvalidArgument(float $price): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Price must be non-negative');

        $this->calculator->calculateGross(net: $price);
    }

    /** @return array<string, array{float}> */
    public static function negativePriceProvider(): array
    {
        return [
            'minus one cent' => [-0.01],
            'large negative' => [-999.99],
        ];
    }
}

3. Checkliste: Laufzeitoptimierung und Test-Isolation

Langsame Tests sind Tests, die nicht ausgeführt werden. Die häufigsten Ursachen für langsame PHPUnit-Suiten sind: unnötige Datenbankzugriffe in Unit-Tests, nicht gecachte Fixtures, fehlende Test-Isolation (gemeinsamer State zwischen Tests) und Coverage aktiviert, obwohl sie nicht benötigt wird. Jeder dieser Punkte lässt sich mit einer gezielten Maßnahme beheben.

Test-Isolation bedeutet: jeder Test startet in einem definierten, sauberen Zustand und hinterlässt keinen State für den nächsten Test. Statische Variablen, Singleton-Instanzen und globale Konfigurationen sind die häufigsten Quellen für Test-Kontamination. PHPUnit führt Tests standardmäßig sequenziell aus; durch Parallelisierung mit paratest oder phpunit --processes lassen sich Laufzeiten je nach Suite um 50–80% reduzieren, wenn Tests vollständig isoliert sind. Ohne Isolation führt Parallelisierung zu sporadischen Fehlern, die schwer zu reproduzieren sind.


<?php

declare(strict_types=1);

namespace Mironsoft\Catalog\Test\Unit\Model;

use Mironsoft\Catalog\Api\ProductRepositoryInterface;
use Mironsoft\Catalog\Model\ProductService;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

/**
 * Checklist: Test isolation — no shared state, no real I/O.
 * Mocks replace all external dependencies.
 * setUp() runs before EACH test — no state leaks between methods.
 */
final class ProductServiceTest extends TestCase
{
    // Typed property — reset in setUp() before every test
    private MockObject&ProductRepositoryInterface $repositoryMock;
    private ProductService $service;

    protected function setUp(): void
    {
        // Fresh mock for every test — no state from previous tests
        $this->repositoryMock = $this->createMock(ProductRepositoryInterface::class);
        $this->service = new ProductService(repository: $this->repositoryMock);
    }

    public function testGetById_WhenProductExists_ReturnsProduct(): void
    {
        $productMock = $this->createConfiguredMock(
            \Mironsoft\Catalog\Api\Data\ProductInterface::class,
            ['getId' => 42, 'getSku' => 'TEST-001']
        );

        $this->repositoryMock
            ->expects($this->once())  // Verifies exactly 1 call — no unnecessary DB hits
            ->method('getById')
            ->with(42)
            ->willReturn($productMock);

        $result = $this->service->getById(42);

        self::assertSame(42, $result->getId());
    }

    protected function tearDown(): void
    {
        // PHPUnit resets mocks automatically, but explicit cleanup
        // is good practice for resources (open files, connections)
        unset($this->repositoryMock, $this->service);
    }
}

4. Checkliste: Docker-Setup für PHPUnit

Ein funktionsfähiges Docker-Setup für PHPUnit erfordert drei Komponenten: die richtige PHP-Version mit den notwendigen Erweiterungen (Xdebug oder PCOV für Coverage, intl, mbstring für Magento), eine separate Testdatenbank, die von der Entwicklungsdatenbank isoliert ist, und korrekte Berechtigungen für die Cache- und Logverzeichnisse. Das häufigste Problem in Docker-PHPUnit-Setups: Tests schreiben in dieselbe Datenbank wie die Entwicklungsinstanz, was zu Datenverlust oder inkonsistenten Tests führt.

Für Magento-Integrationstests muss die etc/install-config-mysql.php auf die Testdatenbank zeigen, nicht auf die Entwicklungsdatenbank. Diese Datei sollte nicht in der Versionskontrolle liegen (enthält Credentials), aber ein Template (install-config-mysql.php.dist) sollte vorhanden sein. Das Mark-Shust-Setup bietet den Wrapper bin/magento für alle Magento-Befehle im Container; PHPUnit für Unit-Tests kann direkt über bin/cli vendor/bin/phpunit aufgerufen werden.

5. Checkliste: PhpStorm-Integration

Eine vollständige PhpStorm-Integration für PHPUnit bedeutet: Tests starten per Klick oder Tastenkürzel, Fehlermeldungen öffnen direkt die betroffene Zeile, Coverage wird im Editor sichtbar und Run Configurations sind im Team versioniert. Die häufigsten Integrationslücken in der Praxis: der Remote-Interpreter zeigt auf eine falsche PHP-Version, Coverage ist nicht konfiguriert oder zu langsam, um nützlich zu sein, und Run Configurations existieren nur auf einem Entwicklerrechner ohne Team-Synchronisierung.

Das Tastenkürzel Ctrl+Shift+F10 führt in PhpStorm den Test unter dem Cursor aus – ohne eine Run Configuration anlegen zu müssen. Ctrl+Shift+R führt die zuletzt ausgeführte Konfiguration erneut aus. Diese zwei Tastenkürzel sind der Kern eines schnellen TDD-Zyklus in PhpStorm. Wer sie nicht kennt, verliert bei jedem Test-Lauf mehrere Sekunden durch Mausbewegungen und Menüinteraktion.

6. Checkliste: Magento-spezifische Muster

Magento-Projekte haben spezifische Anforderungen an PHPUnit, die über Standard-PHP-Tests hinausgehen. Unit-Tests in Magento testen Klassen ohne Magento-Bootstrap – das bedeutet: kein ObjectManager, kein DI-Container, keine echten Repositories. Stattdessen werden alle Abhängigkeiten als Mocks übergeben. Integrationstests verwenden den Magento-Bootstrap und den echten ObjectManager, um realistische Szenarien zu testen – zum Preis einer Laufzeit von Sekunden bis Minuten pro Test.

ViewModels sind in Hyvä-Projekten die bevorzugte Abstraktion. Ihr Vorteil für Tests: Sie haben keine Abhängigkeit von Layout-XML oder Block-Klassen und können direkt instanziiert werden. Ein ViewModel mit drei Abhängigkeiten lässt sich in drei Zeilen setUp() vollständig mit Mocks konfigurieren. Magento-Blöcke hingegen haben implizite Abhängigkeiten vom ObjectManager, die Unit-Tests erschweren oder unmöglich machen.


<?php

declare(strict_types=1);

namespace Mironsoft\Catalog\Test\Unit\ViewModel;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Mironsoft\Catalog\ViewModel\ProductPriceViewModel;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

/**
 * Magento-specific checklist: ViewModel tests.
 * No ObjectManager, no DI container, no Magento bootstrap.
 * All dependencies injected as mocks — fast, isolated, reliable.
 */
#[CoversClass(ProductPriceViewModel::class)]
final class ProductPriceViewModelTest extends TestCase
{
    private ProductPriceViewModel $viewModel;

    protected function setUp(): void
    {
        $scopeConfigMock = $this->createConfiguredMock(
            ScopeConfigInterface::class,
            ['getValue' => '1', 'isSetFlag' => true]
        );

        $priceCurrencyMock = $this->createConfiguredMock(
            PriceCurrencyInterface::class,
            ['format' => '€ 99,00', 'convertAndFormat' => '€ 99,00']
        );

        // ViewModel: no ObjectManager needed — direct injection possible
        $this->viewModel = new ProductPriceViewModel(
            scopeConfig: $scopeConfigMock,
            priceCurrency: $priceCurrencyMock
        );
    }

    #[Test]
    public function testIsPriceDisplayEnabled_WhenConfigEnabled_ReturnsTrue(): void
    {
        self::assertTrue($this->viewModel->isPriceDisplayEnabled());
    }

    #[Test]
    public function testFormatPrice_WithValidAmount_ReturnsFormattedString(): void
    {
        $formatted = $this->viewModel->formatPrice(99.00);

        self::assertStringContainsString('99', $formatted);
    }
}

7. Produktiv vs. kontraproduktiv: Direktvergleich

Viele Antipatter in PHPUnit-Projekten entstehen nicht aus Unwissen, sondern aus Zeitdruck oder fehlender Konvention. Die folgende Tabelle stellt die häufigsten kontraproduktiven Muster den produktiven Alternativen gegenüber.

Bereich Kontraproduktiv Produktiv Auswirkung
Teststruktur tests/ ohne Hierarchie Test/Unit/ + Test/Integration/ Getrennte Suiten, gezielte Ausführung
Methodennamen testWorks, testOk, test1 testCalculate_EmptyCart_ReturnsZero Fehlermeldungen selbsterklärend
Coverage im TDD Immer aktiviert Deaktiviert im Feedback-Zyklus 3–5× schnellerer Testlauf
Datenbank Shared mit Dev-DB Eigene Testdatenbank im Docker Keine Datenverluste, stabile Tests
Run Configurations Nur lokal, nicht geteilt In Git (.idea/runConfigurations/) Sofort nutzbar für alle Teammitglieder

Die größte Einzelmaßnahme für mehr Produktivität ist in den meisten Teams die Trennung der Test-Suiten: Unit-Tests getrennt von Integrationstests. Das ermöglicht es, in weniger als 30 Sekunden Feedback zu bekommen, ohne auf Integrationstests warten zu müssen. Integrationstests laufen dann einmal in der CI-Pipeline, nicht bei jedem lokalen Code-Durchlauf.

Mironsoft

PHPUnit-Audit, Testarchitektur und Produktivitäts-Setup für PHP-Teams

PHPUnit-Setup auf Produktivität trimmen?

Wir analysieren euer PHPUnit-Setup, identifizieren Geschwindigkeitsbremsen und setzen alle Punkte dieser Checkliste um – von der Teststruktur über Docker-Integration bis zur PhpStorm-Konfiguration für euer Team.

Test-Audit

Bestehende Test-Suite analysieren, Antipatter identifizieren, Laufzeit messen

Struktur-Refactoring

Test-Suiten trennen, Namenskonventionen einführen, phpunit.xml optimieren

Team-Enablement

Docker, PhpStorm und CI-Pipeline konfigurieren, Team-Workshop durchführen

8. Zusammenfassung

Eine produktive PHPUnit-Suite ist kein Zufallsprodukt – sie ist das Ergebnis bewusster Entscheidungen in fünf Bereichen: Struktur, Geschwindigkeit, Docker-Setup, PhpStorm-Integration und Magento-spezifische Muster. Die Struktur entscheidet, ob neue Tests ohne Aufwand hinzugefügt werden können. Die Laufzeit entscheidet, ob Tests im täglichen Zyklus genutzt werden. Das Docker-Setup entscheidet, ob Tests in einer kontrollierten, reproduzierbaren Umgebung laufen. Die PhpStorm-Integration entscheidet, ob das Schreiben und Ausführen von Tests produktiv ist. Die Magento-Muster entscheiden, ob Tests wartbar und erweiterbar bleiben.

Der häufigste Fehler: Teams investieren viel Zeit in das Schreiben von Tests, aber kaum Zeit in das Setup. Das Ergebnis sind Tests, die langsam laufen, schwer zu lesen sind und von neuen Teammitgliedern gemieden werden. Eine halbe Tag Investition in das Setup – Teststruktur, phpunit.xml, Run Configurations, Datenbankisolation – amortisiert sich in einer mittleren Codebasis innerhalb von Tagen durch gesparte Debugzeiten und schnellere Feedback-Zyklen.

Produktive PHPUnit-Checkliste — Das Wichtigste auf einen Blick

Struktur

Test/Unit/ und Test/Integration/ trennen. Methodennamen beschreiben Precondition, Action und Expected Result. Jede Produktionsklasse hat genau eine Testklasse am spiegelbildlichen Pfad.

Geschwindigkeit

Coverage im TDD-Zyklus deaktivieren. Unit-Tests isolieren – kein Datenbankzugriff, keine echten HTTP-Calls. Bei vollständiger Isolation Parallelisierung mit paratest evaluieren.

Docker & PhpStorm

Eigene Testdatenbank im Docker. Remote-Interpreter auf den PHP-Container zeigen. Run Configurations als XML in Git versionieren. Ctrl+Shift+F10 für schnellen Einzeltest-Lauf.

Magento

ViewModels statt Block-Klassen – direkt instanziierbar, kein ObjectManager nötig. Integrationstests nur für Magento-spezifische Szenarien mit echtem DI-Container.

9. FAQ: Produktive PHPUnit-Checkliste

1Wie viel Laufzeit ist für Unit-Tests akzeptabel?
Einzelmodul unter 5 Sekunden, gesamte Unit-Suite unter 60 Sekunden. Darüber werden Tests im Entwicklungszyklus nicht mehr konsistent ausgeführt.
2Wann lohnt sich paratest?
Bei vollständiger Test-Isolation und mehr als 200 Tests. Ohne Isolation erzeugt Parallelisierung sporadische Fehler, die schwerer zu debuggen sind als langsame Tests.
3Muss jede Klasse eine Testklasse haben?
Nein – nur Klassen mit Geschäftslogik und Entscheidungen. Reine DTOs, Delegation und Value Objects ohne Logik können übersprungen werden.
4Unit-Test vs. Integrationstest?
Die Grenze ist der erste echte I/O-Aufruf: Datenbankzugriff, HTTP-Request, Dateisystem. Davor: Unit-Test. Ab dem ersten echten I/O: Integrationstest.
5Unit-Test braucht DB-Verbindung?
Das ist kein Unit-Test. Test in Test/Integration/ verschieben oder Klasse refactoren um DB-Zugriff hinter einem Repository-Interface zu abstrahieren.
6Magento Test-Fixtures verwalten?
@magentoDataFixture nutzen, minimal halten. @magentoDbIsolation rollback stellt den Ursprungszustand nach jedem Test wieder her.
7Ziel für TDD-Feedback-Zyklus?
Unter 3 Sekunden vom Speichern bis zum Ergebnis für Unit-Tests. Über 10 Sekunden unterbricht der Zyklus den Entwicklungsflow.
8.idea/ komplett aus Git ausschließen?
Nein. .idea/runConfigurations/ einchecken, workspace.xml und persönliche Dateien in .gitignore. Viele Templates ignorieren fälschlich die gesamte .idea/.
9Magento-Plugin korrekt testen?
Plugin-Klassen als normale Klassen unit-testen, Subject mocken. Das DI-Intercept-Verhalten selbst ist ein Integrationstest.
10Erkennen ob Tests wirklich isoliert sind?
--order-by=random ausführen. Wenn Tests in anderer Reihenfolge fehlschlagen, gibt es State-Abhängigkeiten. --repeat=N deckt Race Conditions auf.