Zum Hauptinhalt springen

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:

VariableTest-WertGrund
APP_ENVtestingTest-Modus
CACHE_STOREarrayKein Redis noetig
QUEUE_CONNECTIONsyncJobs synchron ausfuehren
SESSION_DRIVERarrayKeine DB-Sessions
MAIL_MAILERarrayKeine echten Mails
BROADCAST_CONNECTIONnullKein Reverb noetig
PULSE_ENABLEDfalsePulse deaktiviert
NIGHTWATCH_ENABLEDfalseNightwatch deaktiviert

Location: phpunit.xml

Factories

Alle Factories liegen in database/factories/ und werden von Tests intensiv wiederverwendet:

FactoryModelWichtige Felder
UserFactoryUsername, email, WorkOS-Felder
SocialProfileFactorySocialProfileplatform, handle, canonical_url, Status-Flags
SocialProfileDailyMetricFactorySocialProfileDailyMetricfollowers_count, following_count, date
CollectorClientFactoryCollectorClientname (Sanctum-Token-faehig)
CollectorJobFactoryCollectorJobstatus, 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:

  • CarbonDate Facade
  • $casts Property → casts() Methode
  • scopeX()#[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.