statt nur darüber reden
Test-Driven Development wird oft entweder verteufelt oder als Allheilmittel angepriesen. Die Realität in PHP-Projekten ist differenzierter: TDD funktioniert hervorragend für bestimmte Problemklassen und erzeugt unnötigen Overhead für andere. Dieser Beitrag zeigt, wo der Unterschied liegt – mit Code, nicht mit Theorie.
Inhaltsverzeichnis
- 1. Das Versprechen von TDD – und die Realität
- 2. Der Red-Green-Refactor-Zyklus konkret erklärt
- 3. Erster TDD-Zyklus: Preisberechnung von Null
- 4. TDD für Services mit Abhängigkeiten
- 5. Wo TDD wirklich hilft
- 6. Wo TDD Overhead erzeugt
- 7. TDD in Magento-Projekten pragmatisch
- 8. TDD im Team einführen ohne Dogmatismus
- 9. TDD vs. Test-after im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Das Versprechen von TDD – und die Realität
Test-Driven Development verspricht, dass das Schreiben von Tests vor dem Produktionscode zu besserem Design führt. Das ist keine leere Behauptung – der Mechanismus dahinter ist real: Wenn man gezwungen ist, einen Test zu schreiben, bevor eine Klasse existiert, muss man die Schnittstelle dieser Klasse aus der Nutzerperspektive entwerfen. Das führt tendenziell zu fokussierteren Klassen mit klarerer Verantwortung, weil man nicht mehr Features einbaut als der Test verlangt.
Die Realität in PHP-Projekten sieht oft anders aus. Entwickler probieren TDD aus, schreiben drei Tests für eine neue Funktion, stoßen dann auf Datenbankzugriffe, komplexe Magento-Abhängigkeiten oder externe APIs – und geben auf. Das Problem liegt nicht in TDD selbst, sondern darin, dass TDD ohne testbaren Code nicht funktioniert. TDD ist kein Werkzeug, das man auf beliebigen Code anwendet. Es ist ein Design-Feedback-System, das voraussetzt, dass die Architektur Testbarkeit ermöglicht. Deshalb muss man zuerst verstehen, wo TDD funktioniert und wo nicht.
Ein weiterer Frustrationspunkt: das Tempo. TDD fühlt sich anfangs langsamer an. Man schreibt mehr Code pro Feature, weil Tests und Produktionscode parallel entstehen. Dieser Overhead ist real – er amortisiert sich durch weniger Debugging, weniger Regression und klareres Design. Aber das merkt man erst nach Wochen oder Monaten, nicht nach dem ersten Sprint.
2. Der Red-Green-Refactor-Zyklus konkret erklärt
Der Red-Green-Refactor-Zyklus ist das Herzstück von TDD und besteht aus drei strikt getrennten Phasen. Red: Ein Test wird geschrieben, der das gewünschte Verhalten beschreibt – und fehlschlägt, weil der Produktionscode noch nicht existiert. Das ist der beabsichtigte Ausgangspunkt. Ein sofort grüner Test, ohne Produktionscode, testet meistens nichts Sinnvolles. Green: Es wird gerade so viel Produktionscode geschrieben, dass der Test besteht. Nicht mehr. Kein vorzeitiges Optimieren, keine zusätzlichen Features, kein "das brauchen wir später". Die einzige Regel: Der Test wird grün.
Refactor: Mit grünen Tests im Rücken wird der Code verbessert. Duplikation entfernen, Namen klären, Abstraktionen einführen. Nach jedem Refactoring-Schritt laufen die Tests. Wenn ein Test rot wird, wurde eine Verhaltensänderung eingeführt – entweder war das beabsichtigt, dann muss der Test angepasst werden, oder es ist ein Refactoring-Fehler. Dieser Zyklus läuft in kleinen Schritten, typischerweise alle zwei bis fünf Minuten.
3. Erster TDD-Zyklus: Preisberechnung von Null
Ein konkretes Beispiel verdeutlicht den Zyklus besser als jede abstrakte Beschreibung. Aufgabe: Ein Service, der den Bruttopreis aus Nettopreis und Mehrwertsteuersatz berechnet. TDD startet mit dem ersten Test, bevor die Klasse existiert.
<?php
declare(strict_types=1);
namespace Mironsoft\Pricing\Test\Unit\Service;
use Mironsoft\Pricing\Service\TaxCalculator;
use PHPUnit\Framework\TestCase;
final class TaxCalculatorTest extends TestCase
{
private TaxCalculator $calculator;
protected function setUp(): void
{
$this->calculator = new TaxCalculator();
}
/** @test */
public function it_calculates_gross_price_from_net_and_tax_rate(): void
{
// Red: TaxCalculator does not exist yet — test fails with class not found
$gross = $this->calculator->calculateGross(net: 100.00, taxRate: 19.0);
$this->assertEqualsWithDelta(119.00, $gross, 0.001);
}
/** @test */
public function it_handles_zero_tax_rate(): void
{
$gross = $this->calculator->calculateGross(net: 50.00, taxRate: 0.0);
$this->assertEqualsWithDelta(50.00, $gross, 0.001);
}
/** @test */
public function it_throws_for_negative_net_price(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Net price must not be negative');
$this->calculator->calculateGross(net: -10.00, taxRate: 19.0);
}
}
Erst nach dem Schreiben aller Tests wird die Klasse erstellt – mit genau so viel Code, wie die Tests verlangen. Das Ergebnis ist eine Klasse mit klarer Schnittstelle, die exakt die dokumentierten Anforderungen erfüllt und nicht mehr.
4. TDD für Services mit Abhängigkeiten
Sobald Services externe Abhängigkeiten haben – Repositories, APIs, Datenbanken – wird TDD ohne Mocking unmöglich. Der Test würde bei jedem Lauf gegen echte Infrastruktur arbeiten und wäre damit langsam, fragil und umgebungsabhängig. PHPUnit bietet mit createMock() und createStub() zwei Mechanismen: Stubs liefern konfigurierte Rückgabewerte ohne Verhaltensverifikation, Mocks verifizieren zusätzlich, wie Abhängigkeiten aufgerufen werden.
Der TDD-Ansatz bei Services mit Abhängigkeiten: Im Test wird zuerst definiert, welche Abhängigkeiten der Service benötigt und wie er mit ihnen interagiert. Das zwingt zu einer klaren Trennung von Businesslogik und Infrastruktur. Ein Service, der direkt Datenbankabfragen enthält, lässt sich nicht sinnvoll mit TDD entwickeln – die notwendige Infrastruktur steht im Weg. TDD erzwingt hier das richtige Design: Repository-Pattern, Dependency Injection, klare Interfaces.
<?php
declare(strict_types=1);
namespace Mironsoft\Inventory\Test\Unit\Service;
use Mironsoft\Inventory\Api\StockRepositoryInterface;
use Mironsoft\Inventory\Service\LowStockNotifier;
use PHPUnit\Framework\TestCase;
final class LowStockNotifierTest extends TestCase
{
/** @test */
public function it_triggers_notification_when_stock_falls_below_threshold(): void
{
// Arrange: define dependency behavior before writing the service
$repository = $this->createMock(StockRepositoryInterface::class);
$repository->method('findBelowThreshold')
->with(5) // threshold
->willReturn(['SKU-001', 'SKU-042']);
$mailer = $this->createMock(\Mironsoft\Notification\MailerInterface::class);
$mailer->expects($this->once())
->method('sendLowStockAlert')
->with(['SKU-001', 'SKU-042']);
// Act
$notifier = new LowStockNotifier($repository, $mailer);
$notifier->checkAndNotify(threshold: 5);
}
/** @test */
public function it_does_not_send_notification_when_stock_is_sufficient(): void
{
$repository = $this->createStub(StockRepositoryInterface::class);
$repository->method('findBelowThreshold')->willReturn([]);
$mailer = $this->createMock(\Mironsoft\Notification\MailerInterface::class);
$mailer->expects($this->never())->method('sendLowStockAlert');
$notifier = new LowStockNotifier($repository, $mailer);
$notifier->checkAndNotify(threshold: 5);
}
}
5. Wo TDD wirklich hilft
TDD entfaltet seinen größten Nutzen bei Businesslogik mit klaren Eingabe-Ausgabe-Beziehungen: Preisberechnungen, Rabattregeln, Validierungslogik, Zustandsautomaten, Parsers. In all diesen Fällen ist die erwartete Ausgabe für gegebene Eingaben vorab klar definierbar. TDD zwingt dazu, jeden Grenzfall explizit zu benennen: Was passiert bei negativen Werten? Bei Null? Bei Übergabe eines leeren Arrays? Diese Fragen stellt man bei TDD zwingend, weil der Test sie abbilden muss. Bei Test-after werden sie oft übersehen.
Ein zweiter starker Einsatzbereich: Bugfixes. Wenn ein Bug gemeldet wird, ist der erste Schritt bei TDD, einen Test zu schreiben, der den Bug reproduziert. Der Test ist rot. Dann wird der Bug gefixt, der Test wird grün. Dieser Test bleibt dauerhaft in der Suite und verhindert, dass derselbe Bug wieder auftritt. Das ist konkrete, nachweisbare Regression-Prevention – kein theoretischer Mehrwert, sondern ein direkt messbarer Effekt im nächsten Deployment.
6. Wo TDD Overhead erzeugt
TDD ist kein universelles Werkzeug. Bei UI-Komponenten, Datenbankschema-Migrationen und dem initialen Setup von Konfigurationsdateien erzeugt es mehr Overhead als Nutzen. Das ist kein Fehler von TDD, sondern eine Frage des Anwendungsbereichs. Magento-Layouts, XML-Konfigurationsdateien und Deployment-Skripte sind typischerweise keine guten TDD-Kandidaten – ihr korrektes Verhalten hängt von der Integration mit dem Gesamtsystem ab, nicht von isolierter Logik.
Auch bei Explorationscode lohnt TDD oft nicht: Wenn man noch nicht weiß, wie die Lösung aussehen wird, ist es schwer, sinnvolle Tests zu schreiben. In solchen Phasen ist Prototyping ohne Tests oft schneller – mit der Maßgabe, dass der ausgereifte Code dann konsequent rückwirkend getestet oder von Grund auf testgetrieben neu geschrieben wird. Die Faustregel: TDD für stabile Anforderungen mit klarer Spezifikation, Prototyping für Exploration, Tests danach für UI und Infrastruktur.
7. TDD in Magento-Projekten pragmatisch
Magento hat eine komplexe Architektur mit tiefem Dependency Injection Container, Event System und Plugin-Mechanismen. Das macht TDD für Magento-spezifische Komponenten schwieriger als für reine PHP-Services. Die pragmatische Strategie: TDD konsequent für die eigene Businesslogik anwenden – Services, Calculator-Klassen, Repositories, Transformer – und für Magento-Framework-Integration auf Integrationstests setzen, die mit dem Container arbeiten.
ViewModels sind ein besonders guter TDD-Kandidat in Magento: Sie enthalten oft Formatierungslogik, Preisberechnung und Datenaufbereitung, die sich vollständig isoliert testen lässt. Ein ViewModel, das Preise formatiert, Verfügbarkeiten berechnet oder Produktdaten strukturiert, hat klare Eingaben und Ausgaben – ideal für TDD. Plugin-Klassen, Observer und Layout-Modifikationen sind dagegen besser mit Integrationstests abgedeckt.
8. TDD im Team einführen ohne Dogmatismus
TDD im Team einzuführen scheitert am häufigsten an zu großem Ehrgeiz: Man erklärt, dass ab sofort jeder Feature-Code mit Tests beginnt, und trifft auf Widerstand von Entwicklern, die unter Zeitdruck stehen und das Gefühl haben, doppelt so lange zu brauchen. Der pragmatische Einstieg: TDD zunächst für Bugfixes verbindlich einführen. Jeder Bug bekommt zuerst einen reproduzierenden Test. Das ist eine kleine, konkrete Veränderung mit sofort sichtbarem Nutzen – der Test zeigt, dass der Bug existiert, und verhindert seine Wiederkehr.
Der nächste Schritt: TDD für neue, isolierte Services und Calculator-Klassen. Diese sind typischerweise frei von Frameworkabhängigkeiten und eignen sich ideal für den Einstieg. Bestehendem Code Tests hinzuzufügen ist oft mühsamer als neuen Code testgetrieben zu entwickeln. Teams, die TDD so einführen – zuerst Bugfixes, dann neue isolierte Logik –, bauen langfristig eine Kultur auf, in der Tests zur selbstverständlichen Praxis werden, statt zur aufgezwungenen Bürde.
9. TDD vs. Test-after im Vergleich
Beide Ansätze haben ihre Berechtigung. Der direkte Vergleich hilft bei der Entscheidung, welchen Ansatz man für welche Aufgabe wählt.
| Kriterium | TDD (Test-first) | Test-after | Empfehlung |
|---|---|---|---|
| Design-Feedback | Direkt – schlechtes Design zeigt sich sofort | Nachgelagert – Design steht bereits fest | TDD für neue Services |
| Geschwindigkeit (kurzfristig) | Langsamer durch doppelten Schreibaufwand | Schneller im ersten Sprint | Test-after für Exploration |
| Grenzfall-Abdeckung | Hoch – Tests definieren Grenzfälle vorab | Niedriger – oft nach Bauchgefühl | TDD für Businesslogik |
| Bugfix-Regression | Eingebaut – Bug zuerst als Test | Möglich, aber nicht erzwungen | TDD für alle Bugfixes |
| Frameworklastiger Code | Schwierig ohne Infrastruktur | Integrationstests besser geeignet | Test-after für Magento-Integration |
Mironsoft
PHP-Entwicklung, TDD-Coaching und Magento-Testing
TDD pragmatisch in eurem PHP-Projekt einführen?
Wir helfen Teams, Test-Driven Development realistisch einzuführen – ohne Dogmatismus, mit klaren Grenzen zwischen TDD-Kandidaten und Test-after-Code, und mit konkretem Coaching direkt am Produktionscode.
TDD-Workshop
Hands-on-Workshop mit eurem Produktionscode – Red-Green-Refactor direkt am echten Projekt
Testarchitektur
Klare Trennung von Unit-, Integrations- und Funktionstests für Magento-Projekte
Code-Review
Review bestehender Tests auf Design-Feedback-Qualität und sinnvolle Testabdeckung
10. Zusammenfassung
TDD in PHP ist kein Allheilmittel und kein Mythos, sondern ein Design-Feedback-System, das in bestimmten Kontexten hervorragend funktioniert und in anderen Overhead erzeugt. Der Red-Green-Refactor-Zyklus führt zu klareren Schnittstellen, besserer Grenzfallabdeckung und eingebautem Regressionschutz bei Bugfixes. Für Magento-Projekte bedeutet das: TDD konsequent für Businesslogik, Services und ViewModels – und pragmatische Integrationstests für Frameworkintegration.
Die wichtigste Erkenntnis: TDD funktioniert nur auf testbarem Code. Wer TDD einführen will, muss gleichzeitig die Architektur für Testbarkeit optimieren – Dependency Injection, Repository-Pattern, klare Interfaces. Teams, die das verstehen, berichten nach sechs Monaten konsequenter TDD-Praxis von deutlich weniger Regressionsfehler und besserem Vertrauen in Refactoring. Das ist der reale Mehrwert – messbar, nicht dogmatisch.
TDD in PHP — Das Wichtigste auf einen Blick
Red-Green-Refactor
Test schreiben → fehlschlagen lassen → minimal grün machen → refactoren. Jede Phase strikt getrennt halten.
Beste Einsatzbereiche
Businesslogik, Preisberechnungen, Validierung, Bugfixes. Nicht für UI, Migrationen oder Framework-Integration.
Magento-Strategie
TDD für Services und ViewModels. Integrationstests für DI-Container und Plugin-Mechanismen. Klare Trennung einhalten.
Teameinführung
Mit Bugfixes starten: jeder Bug zuerst als reproduzierender Test. Dann neue isolierte Services. Kein Big-Bang-Ansatz.