Testing
Framework
Postbox verwendet Pest v4 mit dem Laravel-Plugin. Alle Tests nutzen RefreshDatabase und werden automatisch an Tests\TestCase gebunden.
Location: tests/Pest.php, phpunit.xml
Tests ausfuehren
# Alle Tests
php artisan test
# Nur Unit-Tests
php artisan test --testsuite=Unit
# Nur Feature-Tests
php artisan test --testsuite=Feature
# Einzelner Test nach Dateiname
php artisan test tests/Feature/CollectorJobsApiTest.php
# Filter nach Testname
php artisan test --filter="leases a job only once"
# Parallel ausfuehren (schneller bei vielen Tests)
php artisan test --parallel
Datenbank: SQLite Fallback
Tests laufen primaer gegen PostgreSQL. Wenn PostgreSQL lokal nicht erreichbar ist (z.B. in CI ohne DB-Service), faellt tests/Pest.php automatisch auf SQLite in-memory zurueck:
$pgAvailable = extension_loaded('pdo_pgsql')
&& @fsockopen('127.0.0.1', (int) ($_ENV['DB_PORT'] ?? 5432), $errno, $errstr, 1);
if (! $pgAvailable) {
putenv('DB_CONNECTION=sqlite');
putenv('DB_DATABASE=:memory:');
}
Einschraenkung: PostgreSQL-spezifische Syntax (::bigint, jsonb @>, jsonb_array_elements_text) funktioniert nicht in SQLite. Tests die diese Features nutzen, muessen mit einem Skip-Guard versehen werden:
// Beispiel: TagCloud nutzt PostgreSQL JSONB-Funktionen
it('returns top tags', function () {
// TagCloud uses PostgreSQL jsonb_array_elements_text -- skip on SQLite.
if (DB::connection()->getDriverName() !== 'pgsql') {
$this->markTestSkipped('Requires PostgreSQL');
}
// ...
});
Location: tests/Pest.php, tests/Feature/Explore/TagCloudTest.php, tests/Feature/TagConsolidation/
phpunit.xml Konfiguration
Die Test-Umgebung ueberschreibt folgende Werte:
| Variable | Test-Wert | Grund |
|---|---|---|
APP_ENV | testing | Test-Modus |
CACHE_STORE | array | Kein Redis noetig |
QUEUE_CONNECTION | sync | Jobs synchron ausfuehren |
SESSION_DRIVER | array | Keine DB-Sessions |
MAIL_MAILER | array | Keine echten Mails |
BROADCAST_CONNECTION | null | Kein Reverb noetig |
PULSE_ENABLED | false | Pulse deaktiviert |
NIGHTWATCH_ENABLED | false | Nightwatch deaktiviert |
Location: phpunit.xml
Factories
Alle Factories liegen in database/factories/ und werden von Tests intensiv wiederverwendet:
| Factory | Model | Wichtige Felder |
|---|---|---|
UserFactory | User | name, email, WorkOS-Felder |
SocialProfileFactory | SocialProfile | platform, handle, canonical_url, Status-Flags |
SocialProfileDailyMetricFactory | SocialProfileDailyMetric | followers_count, following_count, date |
CollectorClientFactory | CollectorClient | name (Sanctum-Token-faehig) |
CollectorJobFactory | CollectorJob | status, social_profile_id, leased_by |
// Beispiel: Profil mit Metriken erstellen
$profile = SocialProfile::factory()->create([
'platform' => 'youtube',
'handle' => 'test-channel',
]);
SocialProfileDailyMetric::factory()->create([
'social_profile_id' => $profile->id,
'followers_count' => 10000,
'date' => now()->toDateString(),
]);
Location: database/factories/
Test-Konventionen
Feature-Tests (tests/Feature/)
Fuer HTTP-Endpoints, Livewire-Components, Artisan Commands und API-Routen:
// API-Endpoint testen mit Sanctum-Auth
it('requires authentication for collector endpoints', function () {
$job = CollectorJob::factory()->create();
$this->postJson('/api/collector/jobs/lease')->assertStatus(401);
});
// Authentifizierter Request
it('leases a job only once', function () {
$client = CollectorClient::factory()->create();
Sanctum::actingAs($client);
$this->postJson('/api/collector/jobs/lease')->assertOk();
});
Unit-Tests (tests/Unit/)
Fuer Services, Parser, Helper-Funktionen -- ohne HTTP- oder DB-Overhead wo moeglich:
// Service isoliert testen
it('parses instagram handle input', function () {
$parsed = InstagramUrlParser::parse('@Example.Handle');
expect($parsed)->toMatchArray([
'type' => 'handle',
'value' => 'Example.Handle',
'normalizedUrl' => 'https://www.instagram.com/Example.Handle/',
]);
});
// Exception testen
it('rejects non instagram urls', function () {
InstagramUrlParser::parse('https://example.com/not-instagram');
})->throws(InvalidArgumentException::class);
Event-Testing
use Illuminate\Support\Facades\Event;
it('dispatches event on profile sanitization', function () {
Event::fake([ProfileSanitized::class]);
// ... Action ausfuehren ...
Event::assertDispatched(ProfileSanitized::class);
});
Livewire-Component testen
use Livewire\Livewire;
it('renders dashboard for authenticated user', function () {
$user = User::factory()->create();
Livewire::actingAs($user)
->test(Dashboard\Index::class)
->assertOk();
});
Location: tests/Feature/, tests/Unit/
Teststruktur
tests/
├── Feature/
│ ├── Admin/ # Admin-spezifische Tests
│ ├── Auth/ # Authentifizierung
│ ├── Collector/ # Collector-API Tests
│ ├── Console/ # Artisan Command Tests
│ ├── Explore/ # Explore-Feature Tests
│ ├── Http/ # Controller Tests
│ ├── Livewire/ # Component Tests
│ ├── Middleware/ # Middleware Tests
│ ├── CollectorJobsApiTest.php
│ ├── DashboardRollupTest.php
│ ├── SecurityHeadersTest.php
│ └── ...
├── Unit/
│ ├── Commands/ # Command-Logik isoliert
│ ├── Events/ # Event-Tests
│ ├── Jobs/ # Job-Tests
│ ├── Livewire/ # Component-Logik
│ ├── Models/ # Model-Tests
│ ├── Pulse/ # Pulse Recorder Tests
│ ├── Queue/ # Queue-spezifische Tests
│ ├── InstagramUrlParserTest.php
│ ├── HumanNumberTest.php
│ └── ...
├ ── Pest.php # Pest-Konfiguration + SQLite Fallback
└── TestCase.php # Basis-TestCase
Mutation Testing
Postbox nutzt Pest Mutation Testing (pestphp/pest-plugin-mutate v4) mit PCOV als Coverage-Driver.
# Mutation Testing fuer eine bestimmte Klasse
php artisan test --mutate --class='App\Services\SomeService'
# Mutation Testing fuer einen Pfad
php artisan test --mutate --path=app/Services/
# Alle covered Lines mutieren (langsam, fuer CI)
composer mutate
# Composer Scripts
composer mutate # --everything --covered-only
composer mutate:check # --everything --covered-only --parallel (CI-Modus)
Voraussetzung: php8.4-pcov Extension muss installiert sein. Ohne Coverage-Driver schlaegt --mutate mit einem Fehler fehl.
Best Practice: covers() oder mutates() in Testdateien verwenden fuer gezielte Mutation statt --everything:
covers(SomeService::class);
it('does something', function () {
// ...
});
Location: composer.json (Scripts mutate, mutate:check)
Code-Qualitaet
Statische Analyse: Larastan (PHPStan Level 10)
Postbox verwendet Larastan v3 (PHPStan fuer Laravel) auf Level 10 (Maximum) mit einer Baseline fuer bestehende Type-Level-Fehler.
# Analyse ausfuehren
composer phpstan
# Baseline neu generieren (nach Fixes)
composer phpstan:baseline
# Dry-Run (CI-Modus)
composer lint:check
Konfiguration: phpstan.neon (Level 10, Baseline in phpstan-baseline.neon)
Die Baseline enthaelt ~4520 bestehende Type-Level-Fehler (Level 6-10), die schrittweise abgebaut werden. Neue Fehler werden sofort blockiert — nur Fehler in der Baseline sind erlaubt.
Code-Modernisierung: Rector
Rector mit driftingly/rector-laravel modernisiert Code automatisch auf PHP 8.4 und Laravel 12 Standards:
Carbon→DateFacade$castsProperty →casts()MethodescopeX()→#[Scope]Attribute- Dead Code Entfernung
- Type Declarations
- Early Returns
# Rector anwenden
composer rector
# Dry-Run (nur pruefen)
composer rector:check
Konfiguration: rector.php
Code-Formatierung: Laravel Pint
Laravel Pint formatiert Code nach PSR-12 + Laravel Preset.
# Pint anwenden
composer pint
# Dry-Run (nur pruefen)
composer pint:check
Kombination: composer lint
Der empfohlene Workflow kombiniert alle drei Tools in der richtigen Reihenfolge:
# Alle drei: Rector → Pint → PHPStan
composer lint
# Dry-Run fuer CI
composer lint:check
Reihenfolge ist wichtig: Rector aendert Logik → Pint formatiert → PHPStan prueft Typen.
Pre-Commit Pflicht
Vor jedem Commit muessen Lint und Tests gruen sein:
# 1. Rector + Pint + PHPStan
composer lint
# 2. Tests
php artisan test
# 3. Diff pruefen
git diff --staged
Tests und Lint die fehlschlagen blockieren den Commit-Workflow. Bei PostgreSQL-spezifischen Fehlern auf SQLite: den Test mit einem Skip-Guard versehen und die Einschraenkung dokumentieren.