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

# Geavanceerd testen met Pest

> Een systematische uitleg van geavanceerd gebruik van Pest, de standaard sinds Laravel 11: van de Expectation API en datasets tot fakes en Mockery.

## Wat is Pest?

[Pest](https://pestphp.com) is een testframework dat bovenop PHPUnit is gebouwd en dat sinds Laravel 11 standaard wordt gebruikt in nieuwe projecten. Je gebruikt de rijke assertieset van PHPUnit ongewijzigd, maar schrijft tests in een beknopte, closure-gebaseerde syntaxis.

De belangrijkste verschillen met PHPUnit:

| Aspect        | Pest                                 | PHPUnit                  |
| ------------- | ------------------------------------ | ------------------------ |
| Testdefinitie | `test()` / `it()` closures           | Klassen met methodes     |
| Asserties     | Expectation API (`expect()->toBe()`) | `$this->assert*()`       |
| Datasets      | `dataset()` / `with()`               | `@dataProvider`          |
| Hooks         | `beforeEach()` / `afterEach()`       | `setUp()` / `tearDown()` |
| Nesting       | Groeperen met `describe()`           | Scheiden per klasse      |

<Info>
  Pest-tests worden uitgevoerd door PHPUnit en kunnen dus naast bestaande PHPUnit-tests bestaan. Je voert ze uit met `php artisan test` of `vendor/bin/pest`.
</Info>

## Wanneer gebruik je `describe` / `it` / `test`?

### `test()`

De eenvoudigste definitie. De testnaam wordt direct als beschrijving gebruikt.

```php theme={null}
test('een gebruiker kan inloggen met een e-mailadres', function () {
    $user = User::factory()->create();

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

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

### `it()`

In de stijl van "it should..." schrijf je tests die dicht bij natuurlijke taal liggen.

```php theme={null}
it('stuurt niet-geauthenticeerde gebruikers door naar de loginpagina', function () {
    $response = $this->get('/dashboard');

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

### `describe()`

Groepeert gerelateerde tests. Handig voor het delen van `beforeEach()` en het logisch ordenen binnen een bestand.

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

    it('kan de orderlijst ophalen', function () {
        Order::factory(3)->for($this->user)->create();

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

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

    it('kan de orders van andere gebruikers niet ophalen', function () {
        $other = User::factory()->create();
        Order::factory()->for($other)->create();

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

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

<Tip>
  `describe()` kun je nesten. Te diepe nesting maakt tests echter slecht leesbaar; houd twee niveaus als richtlijn aan.
</Tip>

## De Expectation API

`expect()` is de eigen assertiesyntaxis van Pest. Via methodeketens stapel je voorwaarden op.

### Basisasserties

```php theme={null}
expect($value)->toBe(42);               // Strikte gelijkheid (===)
expect($value)->toEqual(['a' => 1]);    // Losse gelijkheid (==)
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);
```

### Asserties op modellen

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

// Controleren dat het is opgeslagen in de database
expect(User::where('email', 'test@example.com')->exists())->toBeTrue();
```

### De `and()`-keten

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

### Elk element van een array verifiëren met `each()`

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

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

## Geparametriseerde tests met datasets

Om dezelfde logica met meerdere invoerwaarden te testen, gebruik je `dataset()` of een inline `with()`.

### Inline datasets

```php theme={null}
it('geeft een validatiefout bij een ongeldig e-mailadres', function (string $email) {
    $response = $this->post('/register', ['email' => $email]);

    $response->assertInvalid('email');
})->with([
    'platte tekst' => ['not-an-email'],
    'zonder domein' => ['user@'],
    'lege string' => [''],
]);
```

### Benoemde datasets

Definieer datasets in de map `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('geeft een validatiefout bij een ongeldig e-mailadres', function (string $email) {
    $response = $this->post('/register', ['email' => $email]);

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

### Dynamische datasets met closures

```php theme={null}
it('heeft per gebruikersplan verschillende limieten', function (string $plan, int $limit) {
    $user = User::factory()->create(['plan' => $plan]);

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

## Unittests met Mockery

### Een service mocken

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

test('de betaalservice wordt aangeroepen', 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

Anders dan een mock roept een spy de implementatie aan én registreert de aanroepen.

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

test('controleren of de notificatieservice is aangeroepen', function () {
    $spy = $this->spy(NotificationService::class);

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

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

### Partial mocks

Mock alleen bepaalde methodes en gebruik voor de rest de echte implementatie.

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

test('het wegschrijven van het rapportbestand wordt overgeslagen', function () {
    $mock = $this->partialMock(ReportService::class, function (MockInterface $mock) {
        $mock->expects('writeFile')->andReturnNull();
    });

    $result = $mock->generate();

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

## Externe API-aanroepen testen met HTTP-fakes

Met `Http::fake()` stub je responses zonder daadwerkelijke HTTP-requests te versturen.

### Een basisfake

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

test('gebruikersinformatie ophalen van een externe API', 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');
});
```

### Foutresponses testen

```php theme={null}
test('er wordt een exception gegooid wanneer de API een fout teruggeeft', function () {
    Http::fake([
        'api.example.com/*' => Http::response([], 503),
    ]);

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

### Een netwerkstoring simuleren

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

test('een verbindingsfout wordt afgehandeld', function () {
    Http::fake(fn () => throw new ConnectionException('Connection refused'));

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

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

## Events, mail en notificaties faken

### `Event::fake()`

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

test('bij het aanmaken van een order wordt een event afgevuurd', function () {
    Event::fake();

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

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

Je kunt ook alleen specifieke events faken en de rest echt laten verwerken.

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

### `Mail::fake()`

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

test('bij registratie wordt een welkomstmail verstuurd', 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('bij verzending wordt een notificatie verstuurd', 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;
    });
});
```

## Artisan-commando's testen

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

test('het commando dat overbodige data verwijdert werkt', 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();
});
```

### Interactieve commando's testen

```php theme={null}
test('een interactief commando voert de bewerking uit na bevestiging', function () {
    $this->artisan('reports:generate')
        ->expectsQuestion('Wil je doorgaan?', 'yes')
        ->expectsOutput('Het rapport is gegenereerd.')
        ->assertExitCode(0);
});
```

## `RefreshDatabase` en `LazilyRefreshDatabase`

### `RefreshDatabase`

Rolt na elke test de database terug en houdt zo een schone staat aan. De migrations worden uitgevoerd bij de start van de testsuite.

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

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

test('een gebruiker kan worden aangemaakt', function () {
    $user = User::factory()->create(['name' => 'Laravel']);

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

### `LazilyRefreshDatabase`

`RefreshDatabase` controleert de migrations bij elke test, maar `LazilyRefreshDatabase` stelt de migrations uit totdat er een test draait die de database daadwerkelijk wijzigt. Als je veel tests hebt die de database niet aanraken, levert dat snelheidswinst op.

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

<Tip>
  Voor de meeste projecten is `RefreshDatabase` de veilige keuze. Overweeg `LazilyRefreshDatabase` wanneer je testsuite groot wordt en veel tests geen databasebewerkingen doen.
</Tip>

| Aspect                              | `RefreshDatabase`             | `LazilyRefreshDatabase`                                  |
| ----------------------------------- | ----------------------------- | -------------------------------------------------------- |
| Moment van migreren                 | Bij de start van de testsuite | Bij de eerste databasebewerking                          |
| Overhead voor tests zonder database | Ja                            | Nee                                                      |
| Aanbevolen situatie                 | Het algemene geval            | Veel tests waarvan een groot deel geen database gebruikt |

## Code coverage meten

Hiervoor is Xdebug of PCOV nodig.

```bash theme={null}
# Coverage in de terminal tonen
php artisan test --coverage

# Een minimale coverage afdwingen (CI faalt bij een lagere waarde)
php artisan test --coverage --min=80

# Een HTML-rapport genereren
vendor/bin/pest --coverage-html=coverage/

# Trage tests bekijken
php artisan test --profile
```

<Warning>
  Het meten van code coverage verlengt de testduur aanzienlijk. In een CI-omgeving raden we aan dit in een aparte job te doen of alleen bij pull requests uit te voeren.
</Warning>

## Gerelateerde pagina's

<Card title="Introductie tot testen" icon="flask" href="/nl/testing">
  Bekijk de basis van het schrijven van tests in Laravel en het gebruik van `php artisan test`.
</Card>


## Related topics

- [Versiecompatibiliteit van packages beheren](/nl/advanced/package-versioning.md)
- [Aan de slag met Laravel-testen in Pest PHP](/nl/blog/pest-introduction.md)
- [Introductie testen](/nl/testing.md)
- [Laravel-packages testen met Orchestra Testbench](/nl/advanced/package-testing.md)
- [Testen](/nl/packages/laravel-bluesky/testing.md)
