> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Fortgeschrittenes Testen mit Pest

> Systematischer Überblick über fortgeschrittene Pest-Nutzung, das ab Laravel 11 die Standardwahl ist — von Expectation API und Datasets über Fakes bis zu Mockery.

## Was ist Pest?

[Pest](https://pestphp.com) ist ein auf PHPUnit aufbauendes Test-Framework und in neuen Projekten ab Laravel 11 die Standardwahl. Sie können die umfangreichen Assertions von PHPUnit weiter nutzen und Tests dazu in prägnanter, Closure-basierter Syntax schreiben.

Wichtige Unterschiede zu PHPUnit:

| Aspekt         | Pest                                 | PHPUnit                  |
| -------------- | ------------------------------------ | ------------------------ |
| Testdefinition | `test()` / `it()` mit Closure        | Klasse mit Methoden      |
| Assertions     | Expectation API (`expect()->toBe()`) | `$this->assert*()`       |
| Datasets       | `dataset()` / `with()`               | `@dataProvider`          |
| Hooks          | `beforeEach()` / `afterEach()`       | `setUp()` / `tearDown()` |
| Nesting        | Gruppierung mit `describe()`         | Trennung über Klassen    |

<Info>
  Pest-Tests laufen auf PHPUnit — bestehende PHPUnit-Tests können nebeneinander bestehen. Ausführen mit `php artisan test` oder `vendor/bin/pest`.
</Info>

## `describe` / `it` / `test` sinnvoll einsetzen

### `test()`

Die einfachste Form. Der Testname dient als Beschreibung.

```php theme={null}
test('Nutzer kann sich mit E-Mail-Adresse anmelden', function () {
    $user = User::factory()->create();

    $response = $this->post('/login', [
        'email' => $user->email,
        'password' => 'password',
    ]);

    $response->assertRedirect('/dashboard');
});
```

### `it()`

Erlaubt eine natürlichere „it should ..."-Formulierung.

```php theme={null}
it('leitet nicht authentifizierte Nutzer auf die Login-Seite um', function () {
    $response = $this->get('/dashboard');

    $response->assertRedirect('/login');
});
```

### `describe()`

Gruppiert zusammengehörige Tests. Praktisch zum Teilen von `beforeEach()` und zur logischen Gliederung.

```php theme={null}
describe('Bestellverwaltung', function () {
    beforeEach(function () {
        $this->user = User::factory()->create();
        $this->actingAs($this->user);
    });

    it('kann die Bestellliste abrufen', function () {
        Order::factory(3)->for($this->user)->create();

        $response = $this->get('/orders');

        $response->assertOk()->assertJsonCount(3, 'data');
    });

    it('kann keine Bestellungen anderer Nutzer abrufen', function () {
        $other = User::factory()->create();
        Order::factory()->for($other)->create();

        $response = $this->get('/orders');

        $response->assertOk()->assertJsonCount(0, 'data');
    });
});
```

<Tip>
  `describe()` lässt sich verschachteln. Zu tiefe Verschachtelung erschwert die Lesbarkeit — als Faustregel maximal zwei Ebenen.
</Tip>

## Expectation API

`expect()` ist Pests eigene Assertion-Syntax mit Method-Chains.

### Grundlegende Assertions

```php theme={null}
expect($value)->toBe(42);               // strikte Gleichheit (===)
expect($value)->toEqual(['a' => 1]);    // lose Gleichheit (==)
expect($value)->toBeTrue();
expect($value)->toBeFalse();
expect($value)->toBeNull();
expect($value)->not->toBeNull();

expect($string)->toContain('Laravel');
expect($array)->toHaveCount(3);
expect($array)->toHaveKey('email');
expect($array)->toContain('admin');

expect($number)->toBeGreaterThan(0);
expect($number)->toBeLessThanOrEqual(100);
```

### Assertions auf Modellen

```php theme={null}
expect($user)->toBeInstanceOf(User::class);
expect($user->email)->toMatchRegex('/^.+@.+\..+$/');

// Datenbank-Persistenz prüfen
expect(User::where('email', 'test@example.com')->exists())->toBeTrue();
```

### `and()`-Chain

```php theme={null}
expect($response->status())->toBe(200)
    ->and($response->json('name'))->toBe('Laravel')
    ->and($response->json('version'))->toBeGreaterThan(12);
```

### `each()` — jedes Array-Element prüfen

```php theme={null}
$users = User::factory(3)->create();

expect($users)->each(function ($user) {
    $user->toBeInstanceOf(User::class)
        ->email->not->toBeNull();
});
```

## Parametrisierte Tests mit Datasets

Um dieselbe Logik mit mehreren Eingaben zu testen, verwenden Sie `dataset()` oder inline `with()`.

### Inline-Dataset

```php theme={null}
it('markiert ungültige E-Mail-Adressen als Validierungsfehler', function (string $email) {
    $response = $this->post('/register', ['email' => $email]);

    $response->assertInvalid('email');
})->with([
    'Klartext' => ['not-an-email'],
    'ohne Domain' => ['user@'],
    'Leerstring' => [''],
]);
```

### Benannte Datasets

Definieren Sie Datasets im Verzeichnis `tests/Datasets`.

```php theme={null}
// tests/Datasets/InvalidEmails.php
dataset('invalid_emails', [
    'plain' => ['not-an-email'],
    'no-domain' => ['user@'],
    'empty' => [''],
    'spaces' => ['user @example.com'],
]);
```

```php theme={null}
it('markiert ungültige E-Mail-Adressen als Validierungsfehler', function (string $email) {
    $response = $this->post('/register', ['email' => $email]);

    $response->assertInvalid('email');
})->with('invalid_emails');
```

### Dynamische Datasets über Closures

```php theme={null}
it('hat pro Nutzerplan unterschiedliche Limits', function (string $plan, int $limit) {
    $user = User::factory()->create(['plan' => $plan]);

    expect($user->requestLimit())->toBe($limit);
})->with([
    ['free', 100],
    ['pro', 1000],
    ['enterprise', 10000],
]);
```

## Unit-Tests mit Mockery

### Service mocken

```php theme={null}
use App\Services\PaymentGateway;
use Mockery\MockInterface;

test('Der Payment-Service wird aufgerufen', function () {
    $mock = $this->mock(PaymentGateway::class, function (MockInterface $mock) {
        $mock->expects('charge')
            ->with(1000, 'jpy')
            ->andReturn(['status' => 'succeeded']);
    });

    $result = app(PaymentGateway::class)->charge(1000, 'jpy');

    expect($result['status'])->toBe('succeeded');
});
```

### Spies

Spies führen die reale Implementierung aus und protokollieren Aufrufe.

```php theme={null}
use App\Services\NotificationService;

test('Der Notification-Service wurde aufgerufen', function () {
    $spy = $this->spy(NotificationService::class);

    $this->post('/orders', ['amount' => 1000]);

    $spy->shouldHaveReceived('send')->once()->with('order.created');
});
```

### Partial Mocks

Nur einzelne Methoden mocken, den Rest real ausführen.

```php theme={null}
use App\Services\ReportService;
use Mockery\MockInterface;

test('Dateischreiben des Reports überspringen', function () {
    $mock = $this->partialMock(ReportService::class, function (MockInterface $mock) {
        $mock->expects('writeFile')->andReturnNull();
    });

    $result = $mock->generate();

    expect($result)->not->toBeNull();
});
```

## Tests externer API-Aufrufe mit HTTP-Fakes

`Http::fake()` stubbt Responses, sodass keine echten HTTP-Requests gesendet werden.

### Grundlegender Fake

```php theme={null}
use Illuminate\Support\Facades\Http;

test('Nutzerinformationen aus externer API laden', function () {
    Http::fake([
        'https://api.example.com/users/*' => Http::response([
            'id' => 1,
            'name' => 'Laravel User',
        ], 200),
    ]);

    $result = app(UserApiClient::class)->find(1);

    expect($result['name'])->toBe('Laravel User');
    Http::assertSent(fn ($request) => $request->url() === 'https://api.example.com/users/1');
});
```

### Fehler-Response testen

```php theme={null}
test('Wirft Exception, wenn die API einen Fehler liefert', function () {
    Http::fake([
        'api.example.com/*' => Http::response([], 503),
    ]);

    expect(fn () => app(UserApiClient::class)->find(1))
        ->toThrow(\App\Exceptions\ApiUnavailableException::class);
});
```

### Netzwerkfehler simulieren

```php theme={null}
use Illuminate\Http\Client\ConnectionException;

test('behandelt Verbindungsfehler', function () {
    Http::fake(fn () => throw new ConnectionException('Connection refused'));

    $result = app(UserApiClient::class)->findWithFallback(1);

    expect($result)->toBeNull();
});
```

## Fakes für Events, Mails und Notifications

### `Event::fake()`

```php theme={null}
use Illuminate\Support\Facades\Event;
use App\Events\OrderCreated;

test('Beim Erstellen einer Bestellung wird ein Event ausgelöst', function () {
    Event::fake();

    $this->post('/orders', ['product_id' => 1, 'amount' => 1000]);

    Event::assertDispatched(OrderCreated::class, function ($event) {
        return $event->order->amount === 1000;
    });
});
```

Sie können auch nur bestimmte Events faken und andere real verarbeiten lassen.

```php theme={null}
Event::fake([OrderCreated::class]);
```

### `Mail::fake()`

```php theme={null}
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;

test('Bei Registrierung wird eine Willkommens-Mail verschickt', function () {
    Mail::fake();

    $this->post('/register', [
        'name' => 'Test User',
        'email' => 'test@example.com',
        'password' => 'password',
        'password_confirmation' => 'password',
    ]);

    Mail::assertSent(WelcomeEmail::class, function ($mail) {
        return $mail->hasTo('test@example.com');
    });
});
```

### `Notification::fake()`

```php theme={null}
use Illuminate\Support\Facades\Notification;
use App\Notifications\OrderShipped;

test('Beim Versand wird eine Benachrichtigung gesendet', function () {
    Notification::fake();

    $user = User::factory()->create();
    $order = Order::factory()->for($user)->create();

    $this->post("/orders/{$order->id}/ship");

    Notification::assertSentTo($user, OrderShipped::class, function ($notification) use ($order) {
        return $notification->order->id === $order->id;
    });
});
```

## Tests von Artisan-Commands

```php theme={null}
use Illuminate\Support\Facades\Artisan;

test('Der Cleanup-Command entfernt abgelaufene Daten', function () {
    $expired = Order::factory()->create(['expires_at' => now()->subDay()]);
    $valid = Order::factory()->create(['expires_at' => now()->addDay()]);

    $this->artisan('orders:cleanup')
        ->assertSuccessful()
        ->expectsOutput('Cleaned up 1 expired order(s).');

    expect(Order::find($expired->id))->toBeNull();
    expect(Order::find($valid->id))->not->toBeNull();
});
```

### Interaktive Commands testen

```php theme={null}
test('Interaktiver Command läuft nach Bestätigung', function () {
    $this->artisan('reports:generate')
        ->expectsQuestion('Ausführen?', 'yes')
        ->expectsOutput('Report wurde erzeugt.')
        ->assertExitCode(0);
});
```

## `RefreshDatabase` und `LazilyRefreshDatabase`

### `RefreshDatabase`

Rollt die Datenbank nach jedem Test zurück und hält sie sauber. Führt beim Start der Suite die Migrationen aus.

```php theme={null}
// tests/Pest.php
uses(RefreshDatabase::class)->in('Feature');
```

```php theme={null}
use Illuminate\Foundation\Testing\RefreshDatabase;

test('Nutzer kann angelegt werden', function () {
    $user = User::factory()->create(['name' => 'Laravel']);

    expect(User::count())->toBe(1);
    expect($user->name)->toBe('Laravel');
});
```

### `LazilyRefreshDatabase`

Während `RefreshDatabase` für jeden Test Migrationen prüft, wartet `LazilyRefreshDatabase` bis zum ersten DB-Zugriff. Wenn viele Tests die Datenbank gar nicht anfassen, beschleunigt das die Suite.

```php theme={null}
// tests/Pest.php
uses(LazilyRefreshDatabase::class)->in('Feature');
```

<Tip>
  In den meisten Projekten ist `RefreshDatabase` die sichere Wahl. `LazilyRefreshDatabase` lohnt sich, wenn die Suite groß ist und viele Tests ohne DB laufen.
</Tip>

| Aspekt                      | `RefreshDatabase`       | `LazilyRefreshDatabase`                |
| --------------------------- | ----------------------- | -------------------------------------- |
| Zeitpunkt der Migrationen   | Zu Beginn der Testsuite | Beim ersten DB-Zugriff                 |
| Overhead in DB-freien Tests | ja                      | nein                                   |
| Empfohlener Einsatz         | Standardfall            | Große Suite mit vielen DB-freien Tests |

## Code Coverage messen

Erfordert Xdebug oder PCOV.

```bash theme={null}
# Coverage im Terminal anzeigen
php artisan test --coverage

# Mindestwert erzwingen (unterschritten → CI schlägt fehl)
php artisan test --coverage --min=80

# HTML-Report generieren
vendor/bin/pest --coverage-html=coverage/

# Langsame Tests identifizieren
php artisan test --profile
```

<Warning>
  Coverage-Messung verlängert die Testlaufzeit erheblich. In der CI empfiehlt sich ein eigener Job oder das Ausführen nur bei Pull-Requests.
</Warning>

## Verwandte Seiten

<Card title="Einstieg in Tests" icon="flask" href="/de/testing">
  Grundlagen des Testens in Laravel und die Nutzung von `php artisan test`.
</Card>


## Related topics

- [Versionskompatibilität von Paketen verwalten](/de/advanced/package-versioning.md)
- [Laravel-Pakete mit Orchestra Testbench testen](/de/advanced/package-testing.md)
- [Laravel-Tests mit Pest PHP starten](/de/blog/pest-introduction.md)
- [Paketentwicklung mit Testbench Workbench](/de/advanced/package-workbench.md)
- [Einen MCP-Server mit Laravel bauen](/de/advanced/mcp-server.md)
