Testing-Strategien für PHP-Geschäftslogik: Unit Tests, Integration Tests & Legacy-Code testbar machen
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
| Situation | Ohne Tests | Mit Tests |
|---|---|---|
| Bug in Rechnungsberechnung | Kunde meldet nach 3 Wochen, manuelle Korrektur aller Rechnungen | Test schlägt sofort fehl, Fix in 20 Minuten |
| Neuer Rabatt-Typ | Angst vor Seiteneffekten, ausgiebiges manuelles Testen | Änderung + neuer Test in 1 Stunde |
| Refactoring einer Kernklasse | Wird vermieden → Code-Qualität sinkt weiter | Tests als Sicherheitsnetz, Refactoring möglich |
| Entwickler-Onboarding | Wochen, bis neuer Entwickler sich traut, etwas zu ändern | Tests 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:
testVipCustomerGets15PercentAbove100EurostatttestDiscount1 - 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?
| Aspekt | Unit Test | Integration Test |
|---|---|---|
| Geschwindigkeit | Millisekunden | Sekunden |
| Abhängigkeiten | Keine (alles gemockt) | Datenbank, Dateisystem, APIs |
| Was wird getestet? | Logik einer Klasse | Zusammenspiel mehrerer Komponenten |
| Wann einsetzen? | Berechnungen, Validierung, Regeln | Queries, API-Calls, File I/O |
| Fehlersuche | Genau lokalisierbar | Breiter 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
| Double | Zweck | Wann einsetzen |
|---|---|---|
| Stub | Gibt feste Werte zurück | Wenn Sie nur Input kontrollieren müssen |
| Mock | Prüft Interaktion | Wenn Sie prüfen müssen, ob etwas aufgerufen wurde |
| Fake | Vereinfachte Implementierung | Wenn Stub/Mock zu komplex werden |
| Spy | Zeichnet Aufrufe auf | Wenn 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
| Problem | Ohne Trennung | Mit Trennung |
|---|---|---|
| Rabattberechnung testen | Braucht DB-Connection, Testdaten, Fixtures | Reines PHP-Objekt, Test in Millisekunden |
| Neuer Entwickler versteht Logik | Muss DB-Schema, Framework, Config kennen | Liest die Domain-Klasse, fertig |
| Datenbank wechseln (MySQL → PostgreSQL) | Geschäftslogik muss mit geändert werden | Nur Repository-Schicht ändern |
| Bug in Geschäftsregel | Debuggen durch Controller, DB-Layer, Views | Lokalisiert in einer Domain-Klasse |
Die Regeln in der Praxis
- Abhängigkeiten injizieren – nie im Konstruktor instanziieren
- Interfaces für externe Dienste –
Mailer,PaymentGateway,FileStorage - Geschäftslogik von Infrastruktur trennen – reine Berechnungen brauchen kein PDO
- Kleine Klassen, klare Verantwortung – eine Klasse mit 500 Zeilen ist nicht testbar
- 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.