# Testing-Guide: Risk Intelligence Platform ## 📋 Inhaltsverzeichnis 1. [Übersicht](#übersicht) 2. [Test-Framework: Pest](#test-framework-pest) 3. [Test-Struktur](#test-struktur) 4. [Tests ausführen](#tests-ausführen) 5. [Feature Tests schreiben](#feature-tests-schreiben) 6. [Unit Tests schreiben](#unit-tests-schreiben) 7. [Livewire/Volt Tests](#livewirevoltt-tests) 8. [Database Testing](#database-testing) 9. [Test-Factories](#test-factories) 10. [Best Practices](#best-practices) 11. [Coverage & CI/CD](#coverage--cicd) 12. [Troubleshooting](#troubleshooting) --- ## Übersicht Die Risk Intelligence Platform nutzt **Pest v4** als Testing-Framework - eine moderne, ausdrucksstarke Alternative zu PHPUnit. ### Test-Philosophie ✅ **Feature Tests über Unit Tests** - Teste Verhalten, nicht Implementierung ✅ **Factories für Test-Daten** - Konsistente, realistische Test-Daten ✅ **RefreshDatabase** - Jeder Test startet mit sauberer DB ✅ **Arrange-Act-Assert** - Klare Test-Struktur ✅ **Beschreibende Test-Namen** - Tests als lebende Dokumentation ### Aktuelle Test-Coverage ``` tests/ ├── Feature/ # 19 Test-Dateien │ ├── Auth/ # Authentication & 2FA │ ├── Settings/ # User Settings │ ├── Jobs/ # Background Jobs │ └── Livewire/ # Livewire Components └── Unit/ # 1 Test-Datei └── ExampleTest.php ``` **Gesamt:** ~20 Test-Dateien mit 50+ einzelnen Tests --- ## Test-Framework: Pest ### Warum Pest? **Pest v4** bietet moderne Testing-Features: - ✅ **Ausdrucksstarke Syntax** - `test()` statt `public function test...()` - ✅ **Expectations API** - `expect($value)->toBe(true)` - ✅ **Datasets** - Parametrisierte Tests - ✅ **Higher Order Tests** - `->it()` Syntax - ✅ **Browser Testing** - Integriertes Browser-Testing (Pest v4) - ✅ **Parallel Execution** - Schnellere Test-Ausführung ### Pest vs PHPUnit ```php // PHPUnit (alt) class UserTest extends TestCase { public function test_user_can_login() { $this->assertTrue(true); } } // Pest (modern) test('user can login', function () { expect(true)->toBeTrue(); }); ``` --- ## Test-Struktur ### Verzeichnis-Layout ``` tests/ ├── Pest.php # Pest-Konfiguration ├── TestCase.php # Basis Test-Klasse │ ├── Feature/ # Feature Tests (End-to-End) │ ├── Auth/ │ │ ├── AuthenticationTest.php │ │ ├── RegistrationTest.php │ │ ├── PasswordResetTest.php │ │ ├── EmailVerificationTest.php │ │ ├── PasswordConfirmationTest.php │ │ └── TwoFactorChallengeTest.php │ │ │ ├── Settings/ │ │ ├── ProfileUpdateTest.php │ │ ├── PasswordUpdateTest.php │ │ └── TwoFactorAuthenticationTest.php │ │ │ ├── Jobs/ │ │ └── SyncBackendDataPoolTest.php │ │ │ ├── Livewire/ │ │ └── Upload/ │ │ └── IndexTest.php │ │ │ ├── CompanySearchTest.php │ ├── TransactionReviewTest.php │ ├── CompanyTransactionsTest.php │ ├── BackendModelsTest.php │ ├── NavigationTest.php │ ├── DashboardTest.php │ └── ExampleTest.php │ └── Unit/ # Unit Tests (isoliert) └── ExampleTest.php ``` ### Test-Typen | Typ | Zweck | Beispiel | |-----|-------|----------| | **Feature** | End-to-End User Flows | "User kann sich einloggen" | | **Unit** | Einzelne Klassen/Methoden | "calculateRiskScore() gibt korrekten Wert zurück" | | **Browser** | UI-Tests im echten Browser | "Click-Flow durch Transaction Review" | --- ## Tests ausführen ### Basis-Commands ```bash # Alle Tests ausführen php artisan test # Oder direkt mit Pest ./vendor/bin/pest # Nur Feature Tests php artisan test --testsuite=Feature # Nur Unit Tests php artisan test --testsuite=Unit # Parallele Ausführung (schneller) php artisan test --parallel ``` ### Spezifische Tests ```bash # Einzelne Test-Datei php artisan test tests/Feature/CompanySearchTest.php # Test mit bestimmtem Namen php artisan test --filter="user can login" # Test-Gruppe php artisan test --group=auth # Mit Ausgabe-Details php artisan test --verbose # Stopp beim ersten Fehler php artisan test --stop-on-failure ``` ### Output-Formate ```bash # Minimal (nur Zusammenfassung) php artisan test --compact # Mit Coverage (benötigt Xdebug) php artisan test --coverage # Mit Coverage-Minimum php artisan test --coverage --min=80 # HTML Coverage Report php artisan test --coverage-html coverage/ ``` --- ## Feature Tests schreiben Feature Tests testen **User Flows** und **End-to-End Szenarien**. ### Beispiel 1: Authentifizierung ```php create([ 'email' => 'test@example.com', 'password' => bcrypt('password'), ]); // Act - Login-Versuch $response = $this->post('/login', [ 'email' => 'test@example.com', 'password' => 'password', ]); // Assert - Prüfungen $response->assertRedirect('/dashboard'); $this->assertAuthenticated(); }); test('user cannot login with invalid password', function () { $user = User::factory()->create([ 'email' => 'test@example.com', 'password' => bcrypt('password'), ]); $response = $this->post('/login', [ 'email' => 'test@example.com', 'password' => 'wrong-password', ]); $response->assertSessionHasErrors(); $this->assertGuest(); }); ``` ### Beispiel 2: Company Search ```php actingAs(User::factory()->create()); // Act $response = $this->get(route('company-search')); // Assert $response->assertOk(); $response->assertSee('Unternehmensauskunft'); }); test('search filters companies by name', function () { $this->actingAs(User::factory()->create()); // Arrange - Test-Companies erstellen $mercedes = Company::factory()->create([ 'name' => 'Mercedes-Benz Group AG', 'ticker' => 'MBG', ]); $volkswagen = Company::factory()->create([ 'name' => 'Volkswagen AG', 'ticker' => 'VOW', ]); Transaction::factory()->for($mercedes)->create(); Transaction::factory()->for($volkswagen)->create(); // Act - Suche nach "Mercedes" $component = Livewire\Volt\Volt::test('company-search') ->set('search', 'Mercedes'); // Assert - Nur Mercedes gefunden $companies = $component->get('companies'); expect($companies) ->toHaveCount(1) ->and($companies->first()->id)->toBe($mercedes->id); }); ``` ### Beispiel 3: Job Testing ```php insert([ 'transaction_id' => 999, 'corporate_entity' => 'Old Company', 'tx_amount' => 1000.00, 'status' => 'done', 'prompt_id' => 1, 'output_key' => 'test_key', 'content' => 'test content', 'created_at' => now(), 'last_modified_at' => now(), 'synced_at' => now(), ]); expect(DB::table('backend_data_pool')->count())->toBe(1); // Act - Full Sync ausführen $job = new SyncBackendDataPool(fullSync: true, batchSize: 100); $job->handle(); // Assert - Alte Daten entfernt $oldRecord = DB::table('backend_data_pool') ->where('transaction_id', 999) ->first(); expect($oldRecord)->toBeNull(); }); test('incremental sync only adds new records', function () { // Arrange - Initial Sync $job = new SyncBackendDataPool(fullSync: true); $job->handle(); $initialCount = DB::table('backend_data_pool')->count(); // Act - Incremental Sync $incrementalJob = new SyncBackendDataPool(fullSync: false); $incrementalJob->handle(); // Assert - Keine Duplikate $newCount = DB::table('backend_data_pool')->count(); expect($newCount)->toBe($initialCount); }); ``` --- ## Unit Tests schreiben Unit Tests testen **einzelne Klassen/Methoden** isoliert. ### Beispiel 1: KYC Risk Calculator ```php calculate([ 'transaction_score' => 20, 'sanctions' => 0, 'country_risk' => 10, 'pep' => 0, 'corruption' => 0, ]); expect($result['level'])->toBe('low') ->and($result['score'])->toBeLessThan(50); }); test('calculates critical risk with sanctions', function () { $calculator = new KycRiskCalculator(); $result = $calculator->calculate([ 'transaction_score' => 50, 'sanctions' => 100, // Critical! 'country_risk' => 30, 'pep' => 20, 'corruption' => 10, ]); expect($result['level'])->toBe('critical') ->and($result['score'])->toBeGreaterThan(70); }); ``` ### Beispiel 2: Model Methods ```php make([ 'status' => Transaction::STATUS_TRUE_POSITIVE, ]); expect($transaction->statusLabel())->toBe('Kritisches Risiko'); $transaction->status = Transaction::STATUS_CLEARED; expect($transaction->statusLabel())->toBe('Geringes Risiko'); }); test('company has many transactions relationship', function () { $company = Company::factory() ->has(Transaction::factory()->count(3)) ->create(); expect($company->transactions)->toHaveCount(3); expect($company->transactions->first())->toBeInstanceOf(Transaction::class); }); ``` --- ## Livewire/Volt Tests ### Livewire Volt Component Tests ```php actingAs(User::factory()->create()); Volt::test('company-search') ->assertSet('search', '') ->assertOk(); }); test('search input updates component state', function () { $this->actingAs(User::factory()->create()); Volt::test('company-search') ->assertSet('search', '') ->set('search', 'Mercedes') ->assertSet('search', 'Mercedes'); }); test('component displays search results', function () { $this->actingAs(User::factory()->create()); Company::factory()->create(['name' => 'Mercedes-Benz AG']); Volt::test('company-search') ->set('search', 'Mercedes') ->assertSee('Mercedes-Benz AG'); }); test('component calls action method', function () { $this->actingAs(User::factory()->create()); $company = Company::factory()->create(); Volt::test('company-search') ->call('viewCompany', $company->id) ->assertRedirect(route('company.transactions', $company)); }); ``` ### Testing Livewire Properties ```php test('component has required properties', function () { $this->actingAs(User::factory()->create()); Volt::test('company-search') ->assertPropertyWired('search') // wire:model="search" ->assertSee('wire:model.live.debounce.300ms="search"', false); }); test('component computes overview correctly', function () { $this->actingAs(User::factory()->create()); Company::factory() ->has(Transaction::factory()->requiresReview()->count(5)) ->create(); $component = Volt::test('company-search'); $overview = $component->get('overview'); expect($overview) ->toHaveKey('companies') ->toHaveKey('open_alerts') ->toHaveKey('open_volume'); }); ``` --- ## Database Testing ### RefreshDatabase Trait ```php // tests/Pest.php pest()->extend(Tests\TestCase::class) ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) ->in('Feature'); ``` **Was macht RefreshDatabase?** - Migriert DB vor jedem Test - Rollt Änderungen nach jedem Test zurück - Nutzt Transactions für Speed ### Database Assertions ```php use Illuminate\Foundation\Testing\RefreshDatabase; test('company is created in database', function () { $company = Company::factory()->create([ 'name' => 'Test AG', ]); // Assert in DB $this->assertDatabaseHas('companies', [ 'name' => 'Test AG', ]); // Oder mit Pest Expectation expect(Company::where('name', 'Test AG')->exists())->toBeTrue(); }); test('transaction is deleted with company', function () { $company = Company::factory() ->has(Transaction::factory()) ->create(); $transactionId = $company->transactions->first()->id; // Delete Company (CASCADE DELETE) $company->delete(); // Assert Transaction auch gelöscht $this->assertDatabaseMissing('transactions', [ 'id' => $transactionId, ]); }); ``` ### Seeding in Tests ```php test('can filter high risk transactions', function () { // Seed Daten $this->seed(CompanySeeder::class); // Oder inline Company::factory() ->has( Transaction::factory() ->status(Transaction::STATUS_TRUE_POSITIVE) ->count(3) ) ->create(); $highRisk = Transaction::where('status', 'true_positive')->get(); expect($highRisk)->toHaveCount(3); }); ``` --- ## Test-Factories Factories generieren konsistente Test-Daten. ### Company Factory ```php // database/factories/CompanyFactory.php namespace Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; class CompanyFactory extends Factory { public function definition(): array { return [ 'name' => fake()->company(), 'legal_name' => fake()->company() . ' AG', 'ticker' => strtoupper(fake()->lexify('???')), 'sector' => fake()->randomElement([ 'Automotive', 'Technology', 'Finance', 'Healthcare' ]), 'country' => fake()->countryCode(), 'headquarters' => fake()->city() . ', ' . fake()->country(), 'kyc_risk_level' => fake()->randomElement(['low', 'high', 'critical']), 'summary' => fake()->paragraph(), ]; } // Custom States public function highRisk(): static { return $this->state(fn (array $attributes) => [ 'kyc_risk_level' => 'high', ]); } public function criticalRisk(): static { return $this->state(fn (array $attributes) => [ 'kyc_risk_level' => 'critical', ]); } } ``` ### Transaction Factory ```php // database/factories/TransactionFactory.php class TransactionFactory extends Factory { public function definition(): array { return [ 'company_id' => Company::factory(), 'reference' => 'TXN-' . fake()->unique()->randomNumber(6), 'amount' => fake()->randomFloat(2, 100, 1000000), 'currency' => 'EUR', 'counterparty' => fake()->company(), 'counterparty_country' => fake()->countryCode(), 'channel' => fake()->randomElement(['SEPA', 'SWIFT', 'Internal']), 'executed_at' => fake()->dateTimeBetween('-1 year', 'now'), 'risk_score' => fake()->numberBetween(0, 255), 'status' => fake()->randomElement([ Transaction::STATUS_CLEARED, Transaction::STATUS_FALSE_POSITIVE, Transaction::STATUS_TRUE_POSITIVE, ]), 'requires_review' => fake()->boolean(), ]; } // States public function requiresReview(bool $value = true): static { return $this->state(fn (array $attributes) => [ 'requires_review' => $value, ]); } public function status(string $status): static { return $this->state(fn (array $attributes) => [ 'status' => $status, ]); } public function highRisk(): static { return $this->state(fn (array $attributes) => [ 'risk_score' => fake()->numberBetween(150, 200), 'status' => Transaction::STATUS_FALSE_POSITIVE, 'requires_review' => true, ]); } public function criticalRisk(): static { return $this->state(fn (array $attributes) => [ 'risk_score' => fake()->numberBetween(200, 255), 'status' => Transaction::STATUS_TRUE_POSITIVE, 'requires_review' => true, ]); } } ``` ### Factory-Nutzung in Tests ```php // Einfach $company = Company::factory()->create(); // Mit Overrides $company = Company::factory()->create([ 'name' => 'Custom Name AG', ]); // Mit State $company = Company::factory()->highRisk()->create(); // Mit Beziehungen $company = Company::factory() ->has(Transaction::factory()->count(5)) ->create(); // Oder anders herum $transaction = Transaction::factory() ->for(Company::factory()->highRisk()) ->create(); // Mehrere mit State-Chain $companies = Company::factory() ->count(3) ->highRisk() ->create(); // Ohne DB-Speicherung (nur Object) $company = Company::factory()->make(); ``` --- ## Best Practices ### 1. Arrange-Act-Assert Pattern ```php test('user can update profile', function () { // Arrange - Setup $user = User::factory()->create(['name' => 'Old Name']); $this->actingAs($user); // Act - Aktion ausführen $response = $this->put('/profile', [ 'name' => 'New Name', 'email' => $user->email, ]); // Assert - Prüfungen $response->assertRedirect(); expect($user->fresh()->name)->toBe('New Name'); }); ``` ### 2. Beschreibende Test-Namen ```php // ✅ Gut test('user cannot delete other users transactions', function () { ... }); // ❌ Schlecht test('test1', function () { ... }); ``` ### 3. One Concept per Test ```php // ✅ Gut - Ein Test pro Konzept test('validates required name field', function () { ... }); test('validates email format', function () { ... }); test('validates unique email', function () { ... }); // ❌ Schlecht - Zu viel in einem Test test('validates all form fields', function () { // Tests name, email, password, etc... }); ``` ### 4. Nutze Datasets für ähnliche Tests ```php test('validates email format', function (string $email, bool $valid) { $response = $this->post('/register', [ 'email' => $email, 'password' => 'password', ]); if ($valid) { $response->assertSessionHasNoErrors('email'); } else { $response->assertSessionHasErrors('email'); } })->with([ 'valid email' => ['test@example.com', true], 'missing @' => ['testexample.com', false], 'missing domain' => ['test@', false], 'spaces' => ['test @example.com', false], ]); ``` ### 5. beforeEach & afterEach Hooks ```php beforeEach(function () { // Vor jedem Test in dieser Datei $this->user = User::factory()->create(); $this->actingAs($this->user); }); afterEach(function () { // Nach jedem Test // Cleanup falls nötig }); test('can access dashboard', function () { // $this->user ist bereits verfügbar $this->get('/dashboard')->assertOk(); }); ``` ### 6. Test Doubles & Mocking ```php use Illuminate\Support\Facades\Mail; use App\Mail\WelcomeMail; test('sends welcome email on registration', function () { // Arrange - Mail mocken Mail::fake(); // Act $this->post('/register', [ 'name' => 'Test User', 'email' => 'test@example.com', 'password' => 'password', ]); // Assert - Mail wurde gesendet Mail::assertSent(WelcomeMail::class, function ($mail) { return $mail->hasTo('test@example.com'); }); }); ``` --- ## Coverage & CI/CD ### Code Coverage generieren ```bash # Einfacher Coverage-Report php artisan test --coverage # Mit Minimum-Schwellwert php artisan test --coverage --min=80 # HTML Report php artisan test --coverage-html coverage/ # Report öffnen open coverage/index.html ``` ### Coverage-Konfiguration ```xml app app/Console/Commands app/Providers/AppServiceProvider.php ``` ### GitHub Actions Integration ```yaml # .github/workflows/tests.yml name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 8.4 extensions: pdo_pgsql, mbstring, xml - name: Install Dependencies run: composer install --no-interaction - name: Run Tests run: php artisan test --parallel ``` --- ## Troubleshooting ### Problem: "Database file does not exist" **Lösung:** ```bash # SQLite DB erstellen touch database/database.sqlite # Oder in .env.testing DB_CONNECTION=sqlite DB_DATABASE=:memory: ``` --- ### Problem: "Class not found in test" **Lösung:** ```bash composer dump-autoload php artisan optimize:clear ``` --- ### Problem: "RefreshDatabase Migration failed" **Lösung:** ```bash # Migrations prüfen php artisan migrate:status # Rollback & Fresh php artisan migrate:fresh --env=testing ``` --- ### Problem: Tests laufen sehr langsam **Lösungen:** 1. **Parallele Ausführung:** ```bash php artisan test --parallel ``` 2. **SQLite statt PostgreSQL:** ```xml ``` 3. **Bcrypt Rounds reduzieren:** ```xml ``` --- ### Problem: "Too many connections" Error **Ursache:** Zu viele parallele DB-Connections **Lösung:** ```bash # Weniger Prozesse php artisan test --parallel --processes=2 ``` --- ## Nützliche Commands ```bash # Test-Liste anzeigen php artisan test --list-tests # Neue Test-Datei erstellen php artisan make:test CompanyTest # Feature Test php artisan make:test CompanyTest --unit # Unit Test php artisan make:test CompanyTest --pest # Pest Syntax # Test mit Debugging php artisan test --filter="specific test" --stop-on-failure # Watch Mode (re-run bei Änderungen) ./vendor/bin/pest --watch # Nur fehlgeschlagene Tests php artisan test --failed ``` --- ## Test-Checkliste Beim Schreiben neuer Features: - [ ] Feature Test geschrieben? - [ ] Edge Cases getestet? - [ ] Validation getestet? - [ ] Authorization getestet? - [ ] Database-Constraints getestet? - [ ] Error Handling getestet? - [ ] Factories aktualisiert? - [ ] Tests laufen durch (`php artisan test`)? - [ ] Code formatiert (`vendor/bin/pint`)? --- ## Zusammenfassung ### Test-Pyramide ``` /\ / \ Unit Tests (schnell, viele) /____\ / \ / Feature \ Feature Tests (mittel, weniger) /___________\ / \ / Browser \ Browser Tests (langsam, wenige) /_________________\ ``` ### Coverage-Ziele | Bereich | Target Coverage | |---------|----------------| | Models | 90%+ | | Services | 85%+ | | Jobs | 80%+ | | Controllers | 70%+ | | Commands | 60%+ | ### Test-Performance | Anzahl Tests | Laufzeit (sequenziell) | Laufzeit (parallel) | |--------------|------------------------|---------------------| | 50 Tests | ~30s | ~10s | | 100 Tests | ~60s | ~20s | | 200 Tests | ~120s | ~40s | --- **Erstellt:** 2025-11-24 **Version:** 1.0 **Autor:** Risk Intelligence Platform Team