Testbarkeit als Architekturprinzip: testbares PHP-Design
AI generated
<?php
8.4
PHP 8.4 · Clean Code · Testbarkeit · Architektur
Testbarkeit als Architekturprinzip
Testbares Design in PHP 8.4, unabhängig von PHPUnit oder jedem anderen Test-Runner

Testbarkeit entsteht nicht beim Schreiben der Tests, sondern beim Entwurf der Klassen selbst. Wer Abhängigkeiten injiziert, Seiteneffekte isoliert und auf statische Aufrufe verzichtet, baut Code, der sich ohne Umwege testen lässt, ganz gleich welches Test-Framework am Ende zum Einsatz kommt. Dieser Artikel zeigt die Entwurfsprinzipien hinter testbarem PHP-Code jenseits von Assertions und Testrunnern.

18 Min. Lesezeit Dependency Injection · Seams · Pure Functions PHP 8.4 · Framework-unabhängig

1. Warum Testbarkeit eine Architekturentscheidung ist, kein Tool-Problem

Viele Teams sagen "wir testen mit PHPUnit" und glauben damit, das Thema Testbarkeit erledigt zu haben. Tatsächlich entscheidet sich Testbarkeit nicht beim Schreiben der Tests, sondern lange vorher: beim Entwurf der Klassen, beim Schnitt der Methoden und bei der Frage, wie Abhängigkeiten in eine Klasse gelangen. Ein Test-Runner kann keine Struktur erzwingen, die nicht bereits im Produktionscode angelegt ist. Testbarkeit ist deshalb eine Eigenschaft der Architektur, nicht des Werkzeugs, mit dem am Ende assertEquals aufgerufen wird.

Wird Testbarkeit erst nachträglich eingefordert, meist wenn die ersten Tests geschrieben werden sollen, zeigt sich das tatsächliche Ausmaß des Problems. Eine Klasse, die intern per new eine Datenbankverbindung öffnet, eine Datei einliest oder die Systemzeit abfragt, lässt sich ohne Refactoring nicht isoliert testen. Der Aufwand, ein solches Design nachträglich testbar zu machen, übersteigt häufig den Aufwand, es von Anfang an mit testbarem Design im Hinterkopf zu entwerfen. Jede Zeile Code, die ohne Rücksicht auf Testbarkeit geschrieben wird, wird zu technischer Schuld, die beim ersten Testversuch fällig wird.

Testbares Design bedeutet nicht, für jede Eventualität eine Abstraktion einzuziehen. Es bedeutet, bewusste Entscheidungen an den Stellen zu treffen, an denen Abhängigkeiten, Seiteneffekte und globaler Zustand entstehen. Die folgenden Abschnitte behandeln genau diese Entscheidungspunkte: Dependency Injection, Seams, pure Funktionen und den bewussten Umgang mit Zeit, Zufall und I/O. Keiner dieser Punkte hat mit einem konkreten Test-Framework zu tun, sondern mit der Struktur des Produktionscodes selbst.

2. Dependency Injection als Grundvoraussetzung für Testbarkeit

Dependency Injection ist die grundlegendste Voraussetzung für Testbarkeit. Sobald eine Klasse ihre Abhängigkeiten selbst erzeugt, statt sie von außen zu erhalten, lässt sich das Verhalten dieser Abhängigkeiten in einem Test nicht mehr beeinflussen. Ein Constructor, der ein Repository, einen HTTP-Client oder eine Uhr als Parameter entgegennimmt, ermöglicht es, im Test ein Double oder einen einfachen Stub einzusetzen. Ohne diese Injektion bleibt nur der Weg über globalen Zustand oder über das Patchen interner Aufrufe, beides Notlösungen, die echte Testbarkeit nicht herstellen.

PHP 8.4 macht Dependency Injection so kompakt wie nie zuvor. Mit Constructor Property Promotion entfällt die manuelle Zuweisung von Parametern auf Eigenschaften vollständig, die Injektion wird zur reinen Deklaration im Constructor-Kopf. Wichtig für die Testbarkeit ist dabei weniger die Kürze der Syntax als die Disziplin dahinter: Jede Abhängigkeit, die eine Klasse benötigt, muss explizit im Constructor sichtbar sein. Ein new mitten in einer Methode, versteckt zwischen Geschäftslogik, entzieht sich jeder Kontrolle von außen und macht genau diese Methode zum Testbarkeits-Blocker, unabhängig davon wie klein der Rest der Klasse ist.

Der Unterschied zwischen einer hart codierten Abhängigkeit und einer injizierten Schnittstelle zeigt sich am deutlichsten im direkten Vergleich zweier Versionen derselben Klasse. Das folgende Beispiel zeigt zunächst das Antipattern und danach die testbare Alternative mit einem injizierten ClockInterface.


<?php

declare(strict_types=1);

namespace App\Order;

/**
 * Calculates whether an order is still eligible for a discount.
 * Untestable: the DateTime dependency is created inline.
 */
final class DiscountCalculator
{
    public function isEligible(Order $order): bool
    {
        // Hard-coded dependency: cannot be replaced in a test
        $now = new \DateTimeImmutable();

        $deadline = $order->getCreatedAt()->modify('+7 days');

        return $now <= $deadline;
    }
}

<?php

declare(strict_types=1);

namespace App\Order;

/**
 * Calculates whether an order is still eligible for a discount.
 * Testable: the current time is injected via ClockInterface.
 */
final readonly class DiscountCalculator
{
    public function __construct(
        private ClockInterface $clock,
    ) {
    }

    public function isEligible(Order $order): bool
    {
        $now = $this->clock->now();

        $deadline = $order->getCreatedAt()->modify('+7 days');

        return $now <= $deadline;
    }
}

interface ClockInterface
{
    public function now(): \DateTimeImmutable;
}

3. Seams: Wo lässt sich Verhalten austauschen?

Michael Feathers prägte in "Working Effectively with Legacy Code" den Begriff Seam für eine Stelle im Code, an der sich Verhalten ändern lässt, ohne den Code an dieser Stelle selbst zu editieren. Ein Seam ist damit die konkrete, technische Antwort auf die Frage, wie Testbarkeit tatsächlich hergestellt wird: nicht durch abstrakte Prinzipien, sondern durch konkrete Punkte im Code, an denen ein Test eingreifen kann. In objektorientiertem PHP ist der wichtigste Seam-Typ der Object Seam, realisiert über Interfaces und Dependency Injection.

Besonders kritisch für Testbarkeit sind globale Funktionen, die nicht deterministisch sind oder externe Ressourcen berühren: time(), rand(), file_get_contents(), curl_exec(). Diese Funktionen lassen sich in PHP nicht ohne Weiteres durch ein Test-Double ersetzen, weil sie keine Objekte sind, sondern freie Funktionen im globalen Namensraum. Der etablierte Ansatz ist, jede dieser Funktionen hinter einer schmalen Interface-Schicht zu verstecken, einem sogenannten Humble Object. Die Klasse, die curl_exec direkt aufruft, wird dabei bewusst so dünn und simpel gehalten, dass sie kaum eigene Logik enthält und selbst keinen Test braucht. Alle Geschäftslogik, die auf dem Ergebnis dieses Aufrufs aufbaut, arbeitet stattdessen gegen das Interface und ist dadurch vollständig testbar.

Das folgende Beispiel zeigt diesen Seam am Beispiel eines HTTP-Aufrufs: Ein HttpClientInterface kapselt curl_exec vollständig, sodass jede Klasse, die HTTP-Anfragen benötigt, gegen die Abstraktion programmiert statt gegen die konkrete Funktion.


<?php

declare(strict_types=1);

namespace App\Http;

/**
 * Seam around curl_exec: business logic depends only on this interface.
 */
interface HttpClientInterface
{
    /**
     * @param array<string, string> $headers
     */
    public function get(string $url, array $headers = []): HttpResponse;
}

/**
 * The only class in the application allowed to call curl_exec directly.
 * Kept intentionally thin, no business logic, no test needed here.
 */
final class CurlHttpClient implements HttpClientInterface
{
    public function get(string $url, array $headers = []): HttpResponse
    {
        $handle = curl_init($url);
        curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($handle, CURLOPT_HTTPHEADER, $headers);

        $body = curl_exec($handle);
        $status = (int) curl_getinfo($handle, CURLINFO_HTTP_CODE);
        curl_close($handle);

        return new HttpResponse($status, (string) $body);
    }
}

final readonly class HttpResponse
{
    public function __construct(
        public int $statusCode,
        public string $body,
    ) {
    }
}

4. Pure Functions vs. Side Effects: kleine deterministische Bausteine

Eine pure Funktion liefert für dieselben Eingaben immer dieselbe Ausgabe und verändert keinen Zustand außerhalb ihres eigenen Gültigkeitsbereichs. Dieses Konzept der referenziellen Transparenz ist für Testbarkeit von zentraler Bedeutung, weil ein Test für eine pure Funktion ohne Setup, ohne Mocks und ohne Teardown auskommt. Man ruft die Funktion mit Eingabewerten auf und vergleicht das Ergebnis mit dem erwarteten Wert, fertig. Kein Netzwerkzugriff, keine Datenbankverbindung, kein Zeitstempel, der zwischen zwei Testläufen unterschiedlich ausfallen könnte.

In der Praxis lässt sich kaum eine Anwendung vollständig aus puren Funktionen bauen, I/O muss irgendwo stattfinden. Die pragmatische Strategie besteht darin, Seiteneffekte konsequent an den Rand der Anwendung zu drängen und den Kern der Geschäftslogik so pur wie möglich zu halten, ein Muster, das oft als "Functional Core, Imperative Shell" bezeichnet wird. Eine Methode, die gleichzeitig eine Berechnung durchführt und das Ergebnis in eine Datei schreibt, lässt sich nicht isoliert testen, ohne das Dateisystem zu berühren. Trennt man Berechnung und Schreibvorgang in zwei Methoden, wird die Berechnung zur puren Funktion und ist ohne jede Infrastruktur testbar.

Der Effekt auf die Testbarkeit einer Codebasis, die diesen Grundsatz konsequent verfolgt, ist erheblich: Der überwiegende Teil der Logik lässt sich mit einfachen Ein- und Ausgabewerten prüfen, während nur eine dünne Schicht am Rand tatsächlich I/O-Tests benötigt. Das folgende Beispiel zeigt zuerst eine Methode, die Berechnung und Seiteneffekt vermischt, danach die aufgeteilte, testbare Variante.


<?php

declare(strict_types=1);

namespace App\Invoice;

// Before: calculation and side effect mixed in a single method,
// untestable without touching the filesystem.
final class InvoiceReportBefore
{
    public function writeTotal(array $items): void
    {
        $total = 0.0;
        foreach ($items as $item) {
            $total += $item->price * $item->quantity;
        }

        file_put_contents('/var/reports/total.txt', (string) $total);
    }
}

// After: pure calculation, fully testable with plain input/output values.
final class InvoiceCalculator
{
    /**
     * @param array<int, InvoiceItem> $items
     */
    public function calculateTotal(array $items): float
    {
        $total = 0.0;
        foreach ($items as $item) {
            $total += $item->price * $item->quantity;
        }

        return $total;
    }
}

// Side effect isolated at the edge of the application.
final readonly class InvoiceReportWriter
{
    public function __construct(
        private FilesystemInterface $filesystem,
    ) {
    }

    public function write(float $total, string $path): void
    {
        $this->filesystem->put($path, (string) $total);
    }
}

5. Statische Methoden und Singletons als Testbarkeits-Killer

Statische Methodenaufrufe sind einer der zuverlässigsten Testbarkeits-Killer in PHP-Code. Der Grund liegt in der Bindung zur Compile-Zeit: Ein Aufruf wie Logger::write($message) ist fest mit der Klasse Logger verdrahtet, es gibt keine Möglichkeit, in einem Test eine andere Implementierung einzusetzen, ohne den Aufrufer selbst zu ändern. Während eine injizierte Abhängigkeit über ein Interface ausgetauscht werden kann, bleibt ein statischer Aufruf für den Aufrufer unveränderlich, genau das Gegenteil dessen, was ein Seam leisten müsste.

Singletons verschärfen das Problem zusätzlich, weil sie globalen, veränderlichen Zustand über die gesamte Laufzeit der Anwendung hinweg transportieren. Ein Test, der eine Singleton-Instanz manipuliert, kann den Zustand für alle nachfolgenden Tests im selben Prozess verändern, unabhängig von Testreihenfolge oder Testklasse. Das führt zu den berüchtigten Tests, die nur einzeln laufen, aber nicht in der vollständigen Suite, und zu einem Debugging-Aufwand, der in keinem Verhältnis zum ursprünglichen Nutzen des Singletons steht. Der versteckte Zugriff auf eine globale Instanz ist zudem für den Leser einer Methode nicht erkennbar, im Gegensatz zu einer Abhängigkeit, die im Constructor sichtbar deklariert ist.

Die praktische Konsequenz für testbares Design lautet: Statische Utility-Methoden ohne Zustand, etwa reine Berechnungsfunktionen, sind unproblematisch, weil sie sich wie pure Funktionen verhalten. Statische Methoden, die auf Datenbanken, Dateisystem, Zeit oder anwendungsweitem Zustand operieren, müssen in Instanzmethoden einer injizierbaren Klasse überführt werden. Der Umbau ist mechanisch: aus static function wird eine gewöhnliche Methode, aus dem globalen Zugriff eine Constructor-Injektion, und die aufrufende Klasse erhält die Abhängigkeit über ihren eigenen Constructor statt über einen globalen Zugriffspunkt.

6. Interfaces als Vertrag: Program to an interface, not an implementation

"Program to an interface, not an implementation" ist mehr als ein Slogan aus dem Gang-of-Four-Buch, es ist die direkte Voraussetzung dafür, dass Test-Doubles überhaupt eingesetzt werden können. Eine Klasse, die gegen eine konkrete Implementierung statt gegen ein Interface programmiert, kann diese Implementierung in einem Test nicht durch einen Stub oder ein Fake ersetzen, ohne die Sprache selbst zu verbiegen. Erst das Interface schafft den Vertrag, an dem sich sowohl die echte Implementierung als auch das Test-Double orientieren können, ohne dass der Aufrufer den Unterschied bemerkt.

Das Liskov-Substitutionsprinzip ist dabei keine akademische Fußnote, sondern eine harte Anforderung an jedes Test-Double: Ein Mock oder Stub muss sich an jeder Stelle, an der die reale Implementierung erwartet wird, exakt so verhalten, dass der Aufrufer keinen Unterschied bemerkt, außer im konkreten Rückgabewert. Verletzt eine Implementierung dieses Prinzip, etwa weil sie zusätzliche Exceptions wirft, die im Interface nicht dokumentiert sind, wird jedes Test-Double auf Basis dieses Interfaces zu einer unvollständigen Abstraktion der Realität, und Tests bestehen, obwohl die Produktion anders reagiert.

Interface Segregation verstärkt die Testbarkeit zusätzlich: Ein kleines, fokussiertes Interface mit zwei oder drei Methoden lässt sich in einem Test-Double mit minimalem Aufwand implementieren. Ein großes Interface mit fünfzehn Methoden, von denen eine Klasse nur zwei tatsächlich benötigt, zwingt jedes Test-Double dazu, auch die übrigen dreizehn Methoden zu implementieren, meist mit Platzhaltern, die die Absicht des Tests verschleiern. Kleine, rollenspezifische Interfaces, orientiert an dem, was ein Konsument tatsächlich braucht, statt an dem, was eine Klasse insgesamt kann, sind deshalb ein direkter Hebel für einfachere und aussagekräftigere Tests.

7. Zeit, Zufall und I/O kapseln

Zeit, Zufall und Ein-/Ausgabe sind die drei häufigsten Quellen von Nichtdeterminismus in PHP-Anwendungen, und Nichtdeterminismus ist der natürliche Feind eines reproduzierbaren Tests. Ein Test, der auf new DateTimeImmutable() innerhalb der zu testenden Klasse basiert, liefert an einem Montag ein anderes Ergebnis als an einem Dienstag. Die Lösung ist immer dieselbe: Die Quelle des Nichtdeterminismus wird hinter einer Schnittstelle versteckt und als Abhängigkeit injiziert, statt direkt im Code aufgerufen zu werden.

Das Clock-Pattern ersetzt jeden verstreuten Aufruf von new DateTimeImmutable() durch ein injiziertes ClockInterface mit einer einzigen Methode now(). In Tests liefert ein FrozenClock oder ein FixedClock immer denselben, fest definierten Zeitpunkt zurück, wodurch Assertions über zeitabhängiges Verhalten, etwa Ablaufdaten oder Fristen, ohne Race Conditions und ohne Sleep-Aufrufe im Test möglich werden. Dasselbe Prinzip gilt für Zufallszahlen: Ein Randomizer-Interface, seit PHP 8.2 durch die eingebaute Randomizer-Klasse unterstützt, ersetzt direkte Aufrufe von rand() oder random_int() und erlaubt es, in Tests einen vorhersehbaren Zufallsgenerator einzusetzen.

Für Dateisystemzugriffe gilt dieselbe Logik: Eine Filesystem-Abstraktion mit Methoden wie read(), write() und exists() ersetzt direkte Aufrufe von file_get_contents(), file_put_contents() und file_exists(). Die konkrete Implementierung dieser Abstraktion darf im Test entweder durch ein In-Memory-Fake ersetzt werden oder, wo es sinnvoll ist, durch ein temporäres Verzeichnis, ohne dass die Geschäftslogik selbst jemals weiß, ob sie gegen ein echtes Dateisystem oder gegen ein Fake arbeitet. Diese drei Kapselungen, Zeit, Zufall und I/O, entfernen die drei häufigsten Ursachen für flackernde, nicht reproduzierbare Tests aus einer Codebasis.

8. Legacy-Code schrittweise testbar machen

Bestehender Code, der ohne Rücksicht auf Testbarkeit entstanden ist, lässt sich selten in einem einzigen Schritt umbauen. Michael Feathers beschreibt in seinem Standardwerk zu Legacy Code den pragmatischen Einstieg über Characterization Tests: Tests, die nicht das gewünschte, sondern das tatsächliche Verhalten des bestehenden Codes dokumentieren, bevor überhaupt ein Refactoring beginnt. Diese Tests bilden ein Sicherheitsnetz, das beim schrittweisen Umbau in Richtung testbares Design verhindert, dass sich das beobachtbare Verhalten der Anwendung ungewollt ändert.

Zwei Techniken haben sich für den schrittweisen Umbau bewährt. Extract & Override extrahiert einen problematischen Aufruf, etwa new DateTimeImmutable() oder einen direkten Datenbankzugriff, in eine eigene, protected Methode. In einer Testklasse, die von der Originalklasse erbt, wird genau diese Methode überschrieben und liefert einen kontrollierten Wert zurück, ohne den Rest der Klasse anzufassen. Die Sprout-Method-Technik geht den umgekehrten Weg: Statt bestehenden Code zu ändern, wird neue Funktionalität von Anfang an testbar als separate Methode oder Klasse entwickelt und erst am Ende in den Legacy-Code eingehängt.

Beide Techniken haben gemeinsam, dass sie ohne Big-Bang-Rewrite auskommen, ein entscheidender Vorteil in gewachsenen Codebasen, in denen ein vollständiger Neubau wirtschaftlich nicht vertretbar ist. Jeder retrofittete Seam reduziert das Risiko beim nächsten Change an derselben Stelle, weil ein Test nun tatsächlich existiert, der das Verhalten dieser Stelle absichert. Testbarkeit wird so nicht zu einer Eigenschaft, die eine Codebasis entweder hat oder nicht hat, sondern zu einer Richtung, in die sich jede einzelne Änderung bewegen kann.


<?php

declare(strict_types=1);

namespace App\Legacy;

/**
 * Legacy class with a hard-coded time dependency.
 * Extract & Override: the problematic call is moved into its own
 * protected method so a test subclass can override it.
 */
class SubscriptionRenewal
{
    public function isDue(Subscription $subscription): bool
    {
        $now = $this->getCurrentTime();

        return $now >= $subscription->getRenewalDate();
    }

    // Seam: protected so a test subclass can override the return value
    protected function getCurrentTime(): \DateTimeImmutable
    {
        return new \DateTimeImmutable();
    }
}

/**
 * Test-only subclass overriding the seam with a fixed point in time.
 * No mocking framework required, just plain inheritance.
 */
final class TestableSubscriptionRenewal extends SubscriptionRenewal
{
    public function __construct(
        private readonly \DateTimeImmutable $fixedNow,
    ) {
    }

    protected function getCurrentTime(): \DateTimeImmutable
    {
        return $this->fixedNow;
    }
}

9. Testbares Design im Vergleich: Antipatterns vs. empfohlene Patterns

Die folgenden fünf Gegenüberstellungen fassen die häufigsten Antipatterns aus den vorangegangenen Abschnitten zusammen und stellen ihnen das jeweils empfohlene, testbare Pattern gegenüber. Keine der rechten Spalten erfordert ein bestimmtes Test-Framework, sie beschreiben ausschließlich Entwurfsentscheidungen im Produktionscode selbst.

Aufgabe Antipattern Empfohlenes Pattern Vorteil
Zeitabhängige Logik new DateTimeImmutable() im Methodenrumpf ClockInterface injizieren Fester Zeitpunkt im Test möglich
Abhängigkeit erzeugen new Service() innerhalb einer Methode Interface über Constructor injizieren Austauschbar durch Test-Double
Globaler Zugriff Logger::write() statisch LoggerInterface injiziert Kein globaler Zustand, keine Bindung zur Compile-Zeit
Anwendungsweiter Zustand Singleton::getInstance() Injizierter Service mit definiertem Lifecycle Kein geteilter Zustand zwischen Tests
Logik und I/O vermischt Berechnung + file_put_contents() in einer Methode Pure Berechnungsfunktion + separater Writer Berechnung ohne Infrastruktur testbar

In jeder Zeile ist der entscheidende Unterschied nicht die Zeilenzahl oder die Eleganz der Syntax, sondern die Frage, ob sich das Verhalten der jeweiligen Zeile in einem Test austauschen lässt, ohne den Produktionscode selbst zu verändern. Genau das ist die Definition von testbarem Design: Kontrollpunkte, an denen ein Test eingreifen kann, ohne die Klasse zu kennen, die ihn später tatsächlich nutzt.

10. Zusammenfassung

Testbarkeit ist keine Eigenschaft, die ein Test-Runner nachträglich in eine Codebasis einbringt. Sie entsteht am Reißbrett: durch Dependency Injection statt hart codierter Abhängigkeiten, durch Seams an den Stellen, an denen globale Funktionen und externe Ressourcen berührt werden, durch pure Funktionen im Kern der Geschäftslogik und durch den Verzicht auf statische Aufrufe und Singletons. PHP 8.4 macht die konsequente Umsetzung mit Constructor Property Promotion so kompakt, dass es keinen syntaktischen Grund mehr gibt, Abhängigkeiten hart zu codieren.

Für bestehenden Code gilt: Testbares Design lässt sich schrittweise nachrüsten, über Characterization Tests als Sicherheitsnetz und über Techniken wie Extract & Override, ohne dass ein vollständiger Rewrite nötig wird. Der entscheidende Perspektivwechsel besteht darin, Testbarkeit nicht als Aufgabe der Testphase zu betrachten, sondern als durchgehendes Kriterium bei jeder Entwurfsentscheidung, unabhängig davon, mit welchem Framework am Ende getestet wird.

Testbares Design in PHP - Das Wichtigste auf einen Blick

Dependency Injection

Abhängigkeiten über den Constructor injizieren statt sie mit new innerhalb einer Methode zu erzeugen. Grundvoraussetzung für jedes Test-Double.

Seams kapseln

time(), rand(), curl_exec() und file_get_contents() hinter Interfaces verstecken. Das Humble Object bleibt dünn, die Logik dahinter ist testbar.

Statics vermeiden

Statische Aufrufe und Singletons sind zur Compile-Zeit gebunden und lassen sich in Tests nicht ersetzen. Injizierte Interfaces schon.

Pure Functions

Berechnung von Seiteneffekten trennen. Pure Funktionen brauchen kein Setup, kein Teardown, keine Mocks.

11. FAQ: Testbarkeit als Designprinzip

1Was bedeutet Testbarkeit im Kontext von PHP-Architektur?
Wie leicht sich eine Klasse isoliert von ihren Abhängigkeiten prüfen lässt, unabhängig vom eingesetzten Test-Framework.
2Ist Testbarkeit dasselbe wie Testabdeckung?
Nein. Abdeckung misst, wie viel Code Tests ausführen. Testbarkeit misst, wie leicht sich Code überhaupt testen lässt.
3Warum ist new innerhalb einer Methode ein Problem?
Es bindet die Klasse fest an eine konkrete Implementierung, die sich im Test nicht ersetzen lässt. Injektion über den Constructor löst das.
4Was ist ein Seam?
Eine Stelle im Code, an der sich Verhalten austauschen lässt, ohne den Code selbst zu editieren. In PHP meist ein Interface.
5Warum sind statische Methoden schwer zu testen?
Sie sind zur Compile-Zeit fest verdrahtet und lassen sich in einem Test nicht durch eine andere Implementierung ersetzen.
6Test-Double vs. echte Implementierung?
Ein Test-Double erfüllt denselben Vertrag wie die echte Implementierung, liefert aber kontrollierte, vorhersehbare Ergebnisse.
7Muss ich für jede Klasse ein Interface erstellen?
Nein, nur dort, wo eine Abhängigkeit Seiteneffekte hat oder in Tests ersetzt werden muss. Pure Klassen brauchen selten eines.
8Legacy-Code testbar machen ohne Rewrite?
Über Characterization Tests als Sicherheitsnetz und Techniken wie Extract & Override, schrittweise statt in einem Rewrite.
9Was ist eine pure Funktion?
Sie liefert für dieselben Eingaben immer dieselbe Ausgabe ohne Seiteneffekte und braucht deshalb kein Setup oder Mocks im Test.
10Kostet testbares Design mehr Zeit?
Kurzfristig etwas mehr durch Interfaces und Injektion, mittelfristig deutlich weniger durch einfachere Tests und sichere Refactorings.

Mironsoft

PHP-Architektur-Beratung, Legacy-Code-Refactoring und Testbarkeits-Reviews

PHP-Code, der sich wirklich testen lässt?

Wir analysieren bestehende PHP-Klassen auf Testbarkeit, decken hart codierte Abhängigkeiten und versteckte Seiteneffekte auf und bauen sie schrittweise zu testbarem Design um, ganz gleich mit welchem Test-Framework ihr am Ende arbeitet.

Testbarkeits-Review

Wir identifizieren new-Aufrufe, Statics und Singletons, die eure Klassen gegen Tests abschotten.

Architektur-Refactoring

Dependency Injection, Seams und pure Funktionen schrittweise nachrüsten, ohne Big-Bang-Rewrite.

Legacy-Code-Coaching

Characterization Tests und Extract & Override, damit bestehender Code sicher in Richtung Testbarkeit wandert.