Testing-Strategien für PHP-Geschäftslogik
Technologie & Architektur

Testing-Strategien für PHP-Geschäftslogik: Unit Tests, Integration Tests & Legacy-Code testbar machen

Carola Schulte
16. März 2026
14 min

In vielen PHP-Business-Systemen fehlen automatisierte Tests komplett. Nicht weil Entwickler faul sind – sondern weil der Code über Jahre gewachsen ist: globale Zustände, statische Aufrufe, God-Classes und Deadlines.

Die meisten Business-PHP-Systeme sind keine sauberen Greenfield-Projekte. Sie sind über Jahre gewachsen – mit Framework-Updates, wechselnden Entwicklern und Features unter Zeitdruck. „Es funktioniert ja" reicht als Strategie, bis es das nicht mehr tut. Und dann kostet jeder Hotfix ein Vielfaches dessen, was ein Test gekostet hätte.

Dieser Artikel zeigt, wie Sie Testing pragmatisch in solche Systeme einführen – ohne Framework-Magie, mit echten Code-Beispielen und einem klaren Fahrplan für Legacy-Code.

Warum Testing in PHP-Business-Apps oft fehlt (und was es kostet)

Die Gründe sind immer dieselben:

  • „Wir haben keine Zeit für Tests" – der Klassiker, der langfristig mehr Zeit kostet
  • Der Code ist nicht testbar – globaler State, statische Aufrufe, Gott-Klassen
  • Keiner im Team hat Erfahrung – Testing wurde nie eingeführt
  • „Das lohnt sich bei uns nicht" – Irrtum, besonders bei Geschäftslogik

Was fehlende Tests wirklich kosten

SituationOhne TestsMit Tests
Bug in RechnungsberechnungKunde meldet nach 3 Wochen, manuelle Korrektur aller RechnungenTest schlägt sofort fehl, Fix in 20 Minuten
Neuer Rabatt-TypAngst vor Seiteneffekten, ausgiebiges manuelles TestenÄnderung + neuer Test in 1 Stunde
Refactoring einer KernklasseWird vermieden → Code-Qualität sinkt weiterTests als Sicherheitsnetz, Refactoring möglich
Entwickler-OnboardingWochen, bis neuer Entwickler sich traut, etwas zu ändernTests dokumentieren das erwartete Verhalten

Das ist keine Theorie. Jede Stunde, die in Tests fließt, spart erfahrungsgemäß ein Vielfaches an Debugging, Hotfixes und Nachtschichten.

Für Entscheider: Was bringt Testing betriebswirtschaftlich?

Testing ist keine akademische Übung. Es ist Risikomanagement für Software.

Die Zahlen

  • Bug-Kosten nach Phase: Ein Bug in der Produktion zu fixen kostet 10–100x mehr als in der Entwicklung
  • Deployment-Frequenz: Teams mit guter Testabdeckung deployen 4x häufiger
  • Entwickler-Fluktuation: Niemand arbeitet gerne an Code, den man nicht anfassen kann
  • Audit-Sicherheit: Automatisierte Tests dokumentieren, dass Geschäftsregeln korrekt implementiert sind

Wo Testing sich rechnet – und wo Sie selbst rechnen müssen

Die Kosten für Testing lassen sich beziffern: Setup, Schulung, laufende Pflege. Die Einsparungen sind schwerer zu greifen, weil sie von Ihrem konkreten Projekt abhängen. Trotzdem ein Rahmen:

Typische Investition:
  Setup + erste Tests + Schulung         ≈ 3.000–6.000 €
  Laufende Pflege (1–3h/Woche)           ≈ 4.000–12.000 €/Jahr

Typische Einsparungen (stark projektabhängig):
  Vermiedene Produktions-Bugs            → Wie teuer ist ein Bug bei Ihnen?
                                            (Rechnungsfehler? Datenverlust? Ausfall?)
  Schnelleres Onboarding                 → Tests dokumentieren erwartetes Verhalten
  Weniger manuelle Regressionstests      → Wie viel Zeit geht heute dafür drauf?
  Refactoring wird möglich               → Code-Qualität sinkt ohne Tests nur

Pauschale ROI-Zahlen wären unseriös – die hängen davon ab, wie teuer ein Produktions-Bug bei Ihnen ist. In einem internen Tool mit 20 Nutzern anders als in einem Abrechnungssystem mit 5.000 Kunden. Rechnen Sie mit Ihren eigenen Zahlen. Was hat der letzte kritische Bug gekostet? Wie oft passiert das pro Jahr? Dagegen stehen die Testkosten.

Unit Tests für Geschäftslogik — ohne Framework-Magie

Unit Tests prüfen eine einzelne Einheit (Klasse, Methode) isoliert. Für Geschäftslogik sind sie Gold wert – wenn man sie richtig aufbaut.

PHPUnit installieren

composer require --dev phpunit/phpunit
mkdir tests
touch phpunit.xml

Minimale phpunit.xml

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
         colors="true"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="Integration">
            <directory>tests/Integration</directory>
        </testsuite>
    </testsuites>
</phpunit>

Beispiel: Rabattberechnung testen

Die Geschäftslogik:

class DiscountCalculator
{
    public function calculate(Order $order, Customer $customer): Money
    {
        if ($customer->isVip() && $order->total()->isGreaterThan(Money::EUR(10000))) {
            return $order->total()->multiply(0.15); // 15% VIP-Rabatt
        }

        if ($order->total()->isGreaterThan(Money::EUR(5000))) {
            return $order->total()->multiply(0.05); // 5% Mengenrabatt
        }

        return Money::EUR(0);
    }
}

Der Test:

class DiscountCalculatorTest extends TestCase
{
    private DiscountCalculator $calculator;

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

    public function testVipCustomerGets15PercentAbove100Euro(): void
    {
        $order = new Order(Money::EUR(15000));     // 150,00 €
        $customer = Customer::vip();

        $discount = $this->calculator->calculate($order, $customer);

        $this->assertEquals(Money::EUR(2250), $discount); // 22,50 €
    }

    public function testRegularCustomerGets5PercentAbove50Euro(): void
    {
        $order = new Order(Money::EUR(7500));       // 75,00 €
        $customer = Customer::regular();

        $discount = $this->calculator->calculate($order, $customer);

        $this->assertEquals(Money::EUR(375), $discount);  // 3,75 €
    }

    public function testNoDiscountBelowThreshold(): void
    {
        $order = new Order(Money::EUR(3000));       // 30,00 €
        $customer = Customer::regular();

        $discount = $this->calculator->calculate($order, $customer);

        $this->assertEquals(Money::EUR(0), $discount);
    }
}

Was macht gute Unit Tests aus?

  • Arrange-Act-Assert: Klare Dreiteilung – Setup, Ausführung, Prüfung
  • Ein Verhalten pro Test: Nicht drei Dinge gleichzeitig prüfen
  • Sprechende Namen: testVipCustomerGets15PercentAbove100Euro statt testDiscount1
  • Keine Logik im Test: Keine if/else, keine Schleifen im Test selbst
  • Unabhängig: Jeder Test funktioniert isoliert, keine Reihenfolge-Abhängigkeit

Integration Tests mit echter Datenbank (PostgreSQL)

Unit Tests prüfen Logik isoliert. Aber irgendwann muss der Code auch mit der Datenbank sprechen. Dafür gibt es Integration Tests.

Test-Datenbank einrichten

-- Separate Test-Datenbank
CREATE DATABASE myapp_test;
GRANT ALL ON DATABASE myapp_test TO myapp_user;

Basis-Klasse für DB-Tests

abstract class DatabaseTestCase extends TestCase
{
    protected PDO $pdo;

    protected function setUp(): void
    {
        $this->pdo = new PDO(
            'pgsql:host=localhost;dbname=myapp_test',
            'myapp_user',
            'test_password'
        );
        $this->pdo->beginTransaction();
    }

    protected function tearDown(): void
    {
        $this->pdo->rollBack(); // Jeder Test startet sauber
    }
}

Der entscheidende Trick: Transaction Rollback. Jeder Test läuft in einer Transaktion, die am Ende zurückgerollt wird. So bleibt die Datenbank immer sauber – ohne Fixtures neu laden zu müssen.

Repository testen

class InvoiceRepositoryTest extends DatabaseTestCase
{
    private InvoiceRepository $repository;

    protected function setUp(): void
    {
        parent::setUp();
        $this->repository = new InvoiceRepository($this->pdo);
    }

    public function testFindOverdueInvoices(): void
    {
        // Arrange: Testdaten einfügen
        $this->insertInvoice('INV-001', '2026-01-15', 'unpaid'); // überfällig
        $this->insertInvoice('INV-002', '2026-04-15', 'unpaid'); // noch nicht fällig
        $this->insertInvoice('INV-003', '2026-01-10', 'paid');   // bezahlt

        // Act
        $overdue = $this->repository->findOverdue(new DateTimeImmutable('2026-03-16'));

        // Assert
        $this->assertCount(1, $overdue);
        $this->assertEquals('INV-001', $overdue[0]->number());
    }

    private function insertInvoice(string $number, string $dueDate, string $status): void
    {
        $this->pdo->prepare(
            'INSERT INTO invoices (number, due_date, status) VALUES (?, ?, ?)'
        )->execute([$number, $dueDate, $status]);
    }
}

Unit Test vs. Integration Test – wann was?

AspektUnit TestIntegration Test
GeschwindigkeitMillisekundenSekunden
AbhängigkeitenKeine (alles gemockt)Datenbank, Dateisystem, APIs
Was wird getestet?Logik einer KlasseZusammenspiel mehrerer Komponenten
Wann einsetzen?Berechnungen, Validierung, RegelnQueries, API-Calls, File I/O
FehlersucheGenau lokalisierbarBreiter Suchbereich

Die Testpyramide – und wann sie nicht passt

Die klassische Testpyramide empfiehlt 70% Unit Tests, 20% Integration Tests, 10% End-to-End Tests. Das ist ein Startpunkt, keine Regel.

In datengetriebenen PHP-Backends – wo ein Großteil der Logik in SQL-Queries, Transaktionen und Datenbank-Constraints steckt – kann eine Verteilung von 40–50% Integration Tests völlig sinnvoll sein. Ein gemocktes Repository testet nicht, ob Ihr JOIN über drei Tabellen das richtige Ergebnis liefert.

Umgekehrt: Ein System mit viel Berechnungslogik (Preisfindung, Rabattkaskaden, Provisionsmodelle) profitiert stark von Unit Tests, weil die Logik von der Datenbank unabhängig ist.

Passen Sie die Verteilung an Ihr Projekt an. Wo steckt die Komplexität? Dort brauchen Sie die meisten Tests.

Test-Doubles: Mocks, Stubs, Fakes — wann was

Wenn eine Klasse Abhängigkeiten hat (Datenbank, E-Mail, externe API), müssen diese für Unit Tests ersetzt werden. Dafür gibt es Test-Doubles:

Stub: Gibt vordefinierte Antworten

// Stub: Gibt immer den gleichen Kunden zurück
$customerRepo = $this->createStub(CustomerRepository::class);
$customerRepo->method('findById')
    ->willReturn(Customer::vip('Max Mustermann'));

$calculator = new PriceCalculator($customerRepo);
$result = $calculator->calculateFor(customerId: 42, items: $items);
// customerRepo.findById(42) gibt unseren VIP-Kunden zurück

Mock: Prüft, ob eine Methode aufgerufen wurde

// Mock: Prüft, dass eine E-Mail gesendet wird
$mailer = $this->createMock(Mailer::class);
$mailer->expects($this->once())
    ->method('send')
    ->with(
        $this->equalTo('kunde@example.com'),
        $this->stringContains('Rechnung')
    );

$invoiceService = new InvoiceService($mailer);
$invoiceService->sendInvoice($invoice);

Fake: Funktionierende Ersatz-Implementierung

// Fake: In-Memory-Repository statt Datenbank
class InMemoryInvoiceRepository implements InvoiceRepository
{
    private array $invoices = [];

    public function save(Invoice $invoice): void
    {
        $this->invoices[$invoice->id()] = $invoice;
    }

    public function findById(string $id): ?Invoice
    {
        return $this->invoices[$id] ?? null;
    }

    public function findOverdue(DateTimeImmutable $date): array
    {
        return array_filter(
            $this->invoices,
            fn(Invoice $inv) => $inv->isOverdueAt($date)
        );
    }
}

Entscheidungshilfe

DoubleZweckWann einsetzen
StubGibt feste Werte zurückWenn Sie nur Input kontrollieren müssen
MockPrüft InteraktionWenn Sie prüfen müssen, ob etwas aufgerufen wurde
FakeVereinfachte ImplementierungWenn Stub/Mock zu komplex werden
SpyZeichnet Aufrufe aufWenn Sie nach dem Test analysieren wollen

Grundregel: Bevorzugen Sie Stubs gegenüber Mocks. Mocks koppeln Ihren Test an die Implementierung – bei jedem Refactoring brechen die Tests, obwohl das Verhalten stimmt. Stubs prüfen nur, ob das Ergebnis korrekt ist, nicht wie es zustande kam.

Legacy-Code testbar machen: Seams, Extract & Override, Dependency Injection

Das größte Hindernis für Tests: Der existierende Code ist nicht testbar. Klassen instanziieren ihre Abhängigkeiten selbst, nutzen globalen State oder statische Aufrufe. Hier drei bewährte Techniken:

Technik 1: Seams finden

Ein Seam (Naht) ist eine Stelle im Code, an der Sie Verhalten ändern können, ohne den umgebenden Code zu modifizieren.

// Vorher: Nicht testbar – Abhängigkeit hart verdrahtet
class OrderProcessor
{
    public function process(Order $order): void
    {
        $db = Database::getInstance();          // Singleton!
        $mailer = new SmtpMailer('smtp.example.com'); // Hart verdrahtet!

        $db->save($order);
        $mailer->send($order->customer()->email(), 'Bestellung bestätigt');
    }
}
// Nachher: Testbar – Abhängigkeiten injiziert
class OrderProcessor
{
    public function __construct(
        private readonly OrderRepository $repository,
        private readonly Mailer $mailer
    ) {}

    public function process(Order $order): void
    {
        $this->repository->save($order);
        $this->mailer->send($order->customer()->email(), 'Bestellung bestätigt');
    }
}

Technik 2: Extract & Override

Wenn Sie den Konstruktor nicht ändern können (weil zu viele Stellen den Code aufrufen), extrahieren Sie die problematische Stelle in eine überschreibbare Methode:

// Schritt 1: Methode extrahieren
class OrderProcessor
{
    public function process(Order $order): void
    {
        $db = $this->getDatabase();
        $db->save($order);
    }

    protected function getDatabase(): Database  // Extrahiert!
    {
        return Database::getInstance();
    }
}

// Schritt 2: Im Test überschreiben
class TestableOrderProcessor extends OrderProcessor
{
    public PDO $testDb;

    protected function getDatabase(): Database
    {
        return new DatabaseAdapter($this->testDb);
    }
}

Das ist kein schöner Code – aber es ist ein sicherer erster Schritt, um Tests einzuführen, ohne alles umzubauen.

Technik 3: Dependency Injection schrittweise einführen

// Schritt 1: Optional Constructor Injection
class InvoiceService
{
    private Mailer $mailer;

    public function __construct(?Mailer $mailer = null)
    {
        // Produktionscode funktioniert weiter wie bisher
        $this->mailer = $mailer ?? new SmtpMailer(Config::get('smtp'));
    }

    public function sendInvoice(Invoice $invoice): void
    {
        $this->mailer->send(
            $invoice->customer()->email(),
            $this->renderInvoiceMail($invoice)
        );
    }
}

// Im Test: Fake übergeben
$fakeMailer = new InMemoryMailer();
$service = new InvoiceService($fakeMailer);
$service->sendInvoice($invoice);

// Prüfen, dass E-Mail "gesendet" wurde
$this->assertCount(1, $fakeMailer->sentMails());
$this->assertStringContains('Rechnung', $fakeMailer->sentMails()[0]->body());

Der Vorteil: Kein bestehender Code bricht. Produktionscode nutzt weiterhin den Default. Tests können Fakes injizieren.

Wichtig: Optionale Constructor Injection ist ein Übergangsmuster, kein Zielzustand. Sie ermöglicht den ersten Test, ohne alles umzubauen. Aber planen Sie, die optionalen Parameter mittelfristig durch echte Dependency Injection zu ersetzen – sonst bleibt der Workaround für immer im Code, und die Abhängigkeit zum konkreten SmtpMailer verschwindet nie wirklich.

Testbare Architektur: Die eigentliche Lösung

Die vorherigen Abschnitte zeigen, wie Sie Tests in bestehenden Code einführen. Aber der größte Hebel liegt woanders: Wer seine Domäne sauber von der Infrastruktur trennt, hat 80% der Testing-Probleme gar nicht erst. Das ist kein akademisches Ideal – es ist die pragmatischste Entscheidung, die Sie für die Wartbarkeit Ihrer Software treffen können.

Das Kernprinzip: Geschäftslogik kennt keine Infrastruktur

Die meisten Testing-Probleme entstehen, weil Geschäftslogik direkt an Datenbank, E-Mail, Dateisystem oder externe APIs gekoppelt ist. Lösen Sie diese Kopplung, werden Tests trivial.

┌─────────────────────────────────────────────┐
│          Controller / API-Endpunkt          │  ← Nimmt Request entgegen,
│                                             │     gibt Response zurück.
│                                             │     Keine Geschäftslogik!
├─────────────────────────────────────────────┤
│          Application Service                │  ← Orchestriert den Ablauf:
│                                             │     Lädt Daten, ruft Domain auf,
│                                             │     speichert Ergebnis.
├─────────────────────────────────────────────┤
│          Domain / Geschäftslogik            │  ← HIER steckt der Wert.
│                                             │     Reine PHP-Klassen, keine I/O,
│                                             │     keine Abhängigkeiten.
│                                             │     → Unit Tests: trivial
├─────────────────────────────────────────────┤
│          Repository / Infrastruktur         │  ← PDO, SMTP, Filesystem, APIs
│                                             │     → Integration Tests
└─────────────────────────────────────────────┘

Die Domain-Schicht enthält die wertvollsten Tests: Berechnungen, Validierungen, Geschäftsregeln. Diese Klassen haben keine Abhängigkeiten zu Datenbank oder Framework – sie sind reine PHP-Klassen. Und genau deshalb sind sie trivial zu testen.

Warum das der größte Hebel ist

ProblemOhne TrennungMit Trennung
Rabattberechnung testenBraucht DB-Connection, Testdaten, FixturesReines PHP-Objekt, Test in Millisekunden
Neuer Entwickler versteht LogikMuss DB-Schema, Framework, Config kennenLiest die Domain-Klasse, fertig
Datenbank wechseln (MySQL → PostgreSQL)Geschäftslogik muss mit geändert werdenNur Repository-Schicht ändern
Bug in GeschäftsregelDebuggen durch Controller, DB-Layer, ViewsLokalisiert in einer Domain-Klasse

Die Regeln in der Praxis

  1. Abhängigkeiten injizieren – nie im Konstruktor instanziieren
  2. Interfaces für externe DiensteMailer, PaymentGateway, FileStorage
  3. Geschäftslogik von Infrastruktur trennen – reine Berechnungen brauchen kein PDO
  4. Kleine Klassen, klare Verantwortung – eine Klasse mit 500 Zeilen ist nicht testbar
  5. Kein globaler State – keine Singletons, keine statischen Methoden für Business-Logik

Beispiel: Saubere Trennung

// Domain: Reine Geschäftslogik, trivial zu testen.
// Kein PDO, kein Framework, keine Abhängigkeiten.
class ShippingCostCalculator
{
    public function calculate(Address $destination, Weight $weight): Money
    {
        if ($destination->country() === 'DE') {
            return $weight->isGreaterThan(Weight::kg(30))
                ? Money::EUR(899)     // 8,99 € Sperrgut
                : Money::EUR(499);    // 4,99 € Standard
        }

        return Money::EUR(1499);      // 14,99 € International
    }
}

// Application Service: Orchestriert den Ablauf.
// Kennt Domain UND Infrastruktur, enthält aber selbst keine Logik.
class OrderService
{
    public function __construct(
        private readonly ShippingCostCalculator $shippingCalc,
        private readonly OrderRepository $orders,
        private readonly Mailer $mailer
    ) {}

    public function placeOrder(OrderRequest $request): Order
    {
        $shipping = $this->shippingCalc->calculate(
            $request->shippingAddress(),
            $request->totalWeight()
        );

        $order = Order::create($request->items(), $shipping);
        $this->orders->save($order);
        $this->mailer->send($request->email(), 'Bestellung eingegangen');

        return $order;
    }
}

Der ShippingCostCalculator lässt sich mit drei Zeilen Setup testen – kein Mock, kein Stub, keine Datenbank. Der OrderService wird mit Stubs/Fakes für Repository und Mailer getestet. Und das Repository selbst bekommt Integration Tests mit echter Datenbank.

Wie Sie dahin kommen – auch schrittweise

Sie müssen nicht alles auf einmal umbauen. Identifizieren Sie die wertvollste Geschäftslogik in Ihrem System (Preisberechnung, Berechtigungsprüfung, Validierungsregeln) und extrahieren Sie sie als erstes in eigene, abhängigkeitsfreie Klassen. Der Rest kann warten.

Jede Klasse, die Sie aus dem Infrastruktur-Sumpf befreien, ist eine Klasse, die Sie ab sofort zuverlässig testen können. Das ist der Weg von „Legacy ohne Tests" zu „Legacy mit Sicherheitsnetz" – eine Klasse nach der anderen.

CI-Integration: Tests automatisch laufen lassen

Tests, die niemand ausführt, sind wertlos. Automatisieren Sie das.

GitHub Actions (einfachste Variante)

# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_DB: myapp_test
          POSTGRES_USER: myapp_user
          POSTGRES_PASSWORD: test_password
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: pdo_pgsql

      - run: composer install --no-interaction
      - run: vendor/bin/phpunit --testsuite Unit
      - run: vendor/bin/phpunit --testsuite Integration

Lokale Git Hooks (vor jedem Commit)

#!/bin/bash
# .git/hooks/pre-commit

echo "Running unit tests..."
vendor/bin/phpunit --testsuite Unit --no-coverage

if [ $? -ne 0 ]; then
    echo "Tests failed. Commit aborted."
    exit 1
fi

Was die CI prüfen sollte

  • Unit Tests: Bei jedem Push (schnell, < 1 Minute)
  • Integration Tests: Bei jedem Push (mit Test-DB, < 5 Minuten)
  • Static Analysis: PHPStan auf Level 6+ (composer require --dev phpstan/phpstan)
  • Code Style: PHP-CS-Fixer (composer require --dev friendsofphp/php-cs-fixer)

Regel: Kein Merge in den Hauptbranch ohne grüne Tests. Keine Ausnahmen.

Praxis-Checkliste: Testing einführen

Sie haben ein bestehendes Projekt ohne Tests? Hier ist der Fahrplan:

Woche 1–2: Grundlagen

  • PHPUnit installieren und konfigurieren
  • Ersten Test schreiben – für die wichtigste Berechnung im System
  • CI einrichten – Tests laufen automatisch

Woche 3–4: Kritische Pfade absichern

  • Geschäftsregeln identifizieren: Wo steckt die Berechnung, die auf keinen Fall falsch sein darf?
  • Tests für Rechnungen, Rabatte, Berechtigungen schreiben
  • Häufige Bug-Quellen mit Tests absichern

Woche 5–8: Infrastruktur

  • Test-Datenbank einrichten (PostgreSQL)
  • Repository-Tests für komplexe Queries
  • Fakes/Stubs für externe Dienste (E-Mail, Payment, APIs)

Ab Woche 9: Kultur

  • Neue Features nur mit Tests – als Team-Regel
  • Bug-Reports → erst Test, dann Fix
  • Code-Reviews prüfen Tests – nicht nur den Produktionscode
  • Test-Coverage messen – aber nicht als alleinige Metrik

Die wichtigsten Befehle

# Alle Tests ausführen
vendor/bin/phpunit

# Nur Unit Tests
vendor/bin/phpunit --testsuite Unit

# Einzelne Testdatei
vendor/bin/phpunit tests/Unit/DiscountCalculatorTest.php

# Mit Coverage-Report
vendor/bin/phpunit --coverage-html coverage/

# Nur fehlgeschlagene Tests erneut ausführen
vendor/bin/phpunit --order-by=defects --stop-on-failure

Fazit: Tests sind kein Luxus – sie sind Professionalität

Testing in PHP-Business-Anwendungen ist kein Nice-to-have. Es ist der Unterschied zwischen „wir hoffen, dass nichts kaputtgeht" und „wir wissen, dass es funktioniert".

Der wichtigste Schritt ist der erste Test. Nicht 100% Coverage anstreben, sondern die kritischste Geschäftsregel absichern. Dann die nächste. Und die nächste.

Sie brauchen dafür kein Framework, keine komplexe Infrastruktur, keine Woche Setup. PHPUnit, eine Testdatei, und 30 Minuten. Der Rest wächst organisch – wenn Sie anfangen.

  • Fangen Sie bei der Geschäftslogik an – Berechnungen, Validierungen, Regeln
  • Machen Sie Tests zur Gewohnheit – CI erzwingt das automatisch
  • Legacy-Code ist kein Hindernis – Seams, Extract & Override, optionale DI
  • Tests dokumentieren Verhalten – besser als jeder Kommentar

Sie haben ein PHP-Projekt ohne Tests und wollen das ändern? Ich helfe Ihnen, eine Testing-Strategie zu entwickeln und die kritischen Pfade abzusichern – pragmatisch, ohne Overengineering.

Weitere interessante Artikel

PHP ohne Framework – warum?

Warum Framework-freies PHP für Business-Anwendungen oft die bessere Wahl ist.

Legacy-Modernisierung

Strategien für die schrittweise Modernisierung von Alt-Systemen.

REST-API-Design

Saubere APIs für PHP-Business-Anwendungen entwerfen.