# 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
<?php

use App\Models\User;

test('user can login with valid credentials', function () {
    // Arrange - Test-User erstellen
    $user = User::factory()->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
<?php

use App\Models\Company;
use App\Models\Transaction;
use App\Models\User;

test('authenticated users can visit company search page', function () {
    // Arrange
    $this->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
<?php

use App\Jobs\SyncBackendDataPool;
use Illuminate\Support\Facades\DB;

test('full sync truncates and rebuilds data pool', function () {
    // Arrange - Alte Daten einfügen
    DB::table('backend_data_pool')->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
<?php

use App\Services\KycRiskCalculator;

test('calculates low risk correctly', function () {
    $calculator = new KycRiskCalculator();

    $result = $calculator->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
<?php

use App\Models\Transaction;
use App\Models\Company;

test('transaction statusLabel returns correct labels', function () {
    $transaction = Transaction::factory()->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
<?php

use App\Models\User;
use Livewire\Volt\Volt;

test('component initializes with empty search', function () {
    $this->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
<!-- phpunit.xml -->
<source>
    <include>
        <directory>app</directory>
    </include>
    <exclude>
        <directory>app/Console/Commands</directory>
        <file>app/Providers/AppServiceProvider.php</file>
    </exclude>
</source>
```

### 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
<!-- phpunit.xml -->
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
```

3. **Bcrypt Rounds reduzieren:**
```xml
<env name="BCRYPT_ROUNDS" value="4"/>
```

---

### 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
