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

# Tests avancés avec Pest

> Découvrez de manière structurée l'utilisation avancée de Pest, devenu la valeur par défaut depuis Laravel 11 : Expectation API, datasets, fakes et Mockery.

## Qu'est-ce que Pest

[Pest](https://pestphp.com) est un framework de test construit au-dessus de PHPUnit et adopté par défaut dans tout nouveau projet depuis Laravel 11. Il reprend l'ensemble des assertions de PHPUnit tout en offrant une syntaxe concise basée sur des closures.

Principales différences avec PHPUnit :

| Aspect             | Pest                                 | PHPUnit                  |
| ------------------ | ------------------------------------ | ------------------------ |
| Définition de test | closures `test()` / `it()`           | méthodes dans une classe |
| Assertions         | Expectation API (`expect()->toBe()`) | `$this->assert*()`       |
| Datasets           | `dataset()` / `with()`               | `@dataProvider`          |
| Hooks              | `beforeEach()` / `afterEach()`       | `setUp()` / `tearDown()` |
| Imbrication        | regroupement via `describe()`        | séparation par classe    |

<Info>
  Les tests Pest étant exécutés par PHPUnit, ils cohabitent sans problème avec des tests PHPUnit existants. Lancez-les avec `php artisan test` ou `vendor/bin/pest`.
</Info>

## Quand utiliser `describe` / `it` / `test`

### `test()`

La définition la plus simple : le nom du test sert directement de description.

```php theme={null}
test('un utilisateur peut se connecter par e-mail', function () {
    $user = User::factory()->create();

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

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

### `it()`

Permet une écriture proche du langage naturel, sous forme « it should… ».

```php theme={null}
it('redirige les utilisateurs non authentifiés vers la page de connexion', function () {
    $response = $this->get('/dashboard');

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

### `describe()`

Regroupe des tests apparentés. Utile pour partager un `beforeEach()` ou structurer logiquement un fichier.

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

    it('récupère la liste des commandes', function () {
        Order::factory(3)->for($this->user)->create();

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

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

    it("ne récupère pas les commandes des autres utilisateurs", function () {
        $other = User::factory()->create();
        Order::factory()->for($other)->create();

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

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

<Tip>
  `describe()` peut être imbriqué, mais une imbrication trop profonde nuit à la lisibilité. Deux niveaux au maximum sont un bon repère.
</Tip>

## Expectation API

`expect()` est la syntaxe d'assertion propre à Pest. Vous pouvez enchaîner des conditions via des appels de méthodes.

### Assertions de base

```php theme={null}
expect($value)->toBe(42);               // égalité stricte (===)
expect($value)->toEqual(['a' => 1]);    // égalité lâche (==)
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 sur des modèles

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

// Vérifier l'enregistrement en base
expect(User::where('email', 'test@example.com')->exists())->toBeTrue();
```

### Chaîne `and()`

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

### Vérifier chaque élément d'un tableau avec `each()`

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

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

## Tests paramétrés avec les datasets

Pour exécuter la même logique avec plusieurs entrées, utilisez `dataset()` ou `with()` en ligne.

### Dataset en ligne

```php theme={null}
it("une adresse e-mail invalide provoque une erreur de validation", function (string $email) {
    $response = $this->post('/register', ['email' => $email]);

    $response->assertInvalid('email');
})->with([
    'texte brut' => ['not-an-email'],
    'sans domaine' => ['user@'],
    'chaîne vide' => [''],
]);
```

### Datasets nommés

Définissez les datasets dans le répertoire `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("une adresse e-mail invalide provoque une erreur de validation", function (string $email) {
    $response = $this->post('/register', ['email' => $email]);

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

### Dataset dynamique avec closure

```php theme={null}
it("chaque plan utilisateur a sa propre limite", function (string $plan, int $limit) {
    $user = User::factory()->create(['plan' => $plan]);

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

## Tests unitaires avec Mockery

### Mocker un service

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

test('le service de paiement est invoqué', function () {
    $mock = $this->mock(PaymentGateway::class, function (MockInterface $mock) {
        $mock->expects('charge')
            ->with(1000, 'eur')
            ->andReturn(['status' => 'succeeded']);
    });

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

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

### Espion (spy)

Contrairement à un mock, un spy exécute l'implémentation tout en enregistrant les appels.

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

test("vérifier que le service de notifications a été appelé", function () {
    $spy = $this->spy(NotificationService::class);

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

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

### Mock partiel

Mocker certaines méthodes et laisser les autres utiliser l'implémentation réelle.

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

test("l'écriture du fichier de rapport est passée en mock", function () {
    $mock = $this->partialMock(ReportService::class, function (MockInterface $mock) {
        $mock->expects('writeFile')->andReturnNull();
    });

    $result = $mock->generate();

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

## Tests d'appels d'API externes via HTTP fake

`Http::fake()` permet de stubber les réponses sans envoyer de vraie requête HTTP.

### Fake de base

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

test("récupère les infos utilisateur depuis une API externe", 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');
});
```

### Tester une réponse d'erreur

```php theme={null}
test("une erreur d'API provoque une exception", function () {
    Http::fake([
        'api.example.com/*' => Http::response([], 503),
    ]);

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

### Simuler une panne réseau

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

test("gestion d'une erreur de connexion", function () {
    Http::fake(fn () => throw new ConnectionException('Connection refused'));

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

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

## Fake d'événements, e-mails et notifications

### `Event::fake()`

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

test("un événement est dispatché à la création d'une commande", function () {
    Event::fake();

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

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

Vous pouvez ne fake que certains événements et laisser les autres s'exécuter réellement.

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

### `Mail::fake()`

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

test("un e-mail de bienvenue est envoyé à l'inscription", 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("une notification est envoyée lors de l'expédition", 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;
    });
});
```

## Tester une commande Artisan

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

test("la commande de nettoyage fonctionne", 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();
});
```

### Tester une commande interactive

```php theme={null}
test("la commande interactive s'exécute après confirmation", function () {
    $this->artisan('reports:generate')
        ->expectsQuestion('Exécuter ?', 'yes')
        ->expectsOutput('Rapport généré.')
        ->assertExitCode(0);
});
```

## `RefreshDatabase` et `LazilyRefreshDatabase`

### `RefreshDatabase`

Rollback la base après chaque test pour garantir un état propre. Les migrations sont exécutées au démarrage de la suite de tests.

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

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

test("un utilisateur peut être créé", function () {
    $user = User::factory()->create(['name' => 'Laravel']);

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

### `LazilyRefreshDatabase`

Là où `RefreshDatabase` prépare les migrations pour tous les tests, `LazilyRefreshDatabase` diffère les migrations jusqu'à ce qu'un test modifie effectivement la base. Cela accélère les suites où de nombreux tests ne touchent pas à la base.

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

<Tip>
  Pour la plupart des projets, `RefreshDatabase` est un choix sûr. Envisagez `LazilyRefreshDatabase` uniquement quand la suite est grande et comporte beaucoup de tests sans base de données.
</Tip>

| Aspect                            | `RefreshDatabase`     | `LazilyRefreshDatabase`                       |
| --------------------------------- | --------------------- | --------------------------------------------- |
| Moment d'exécution des migrations | démarrage de la suite | premier accès à la base                       |
| Surcoût pour les tests sans base  | oui                   | non                                           |
| Cas d'usage recommandé            | cas général           | grande suite avec beaucoup de tests sans base |

## Mesure de la couverture de code

Xdebug ou PCOV est requis.

```bash theme={null}
# Afficher la couverture dans le terminal
php artisan test --coverage

# Forcer un seuil minimum (échec CI en dessous)
php artisan test --coverage --min=80

# Générer un rapport HTML
vendor/bin/pest --coverage-html=coverage/

# Identifier les tests lents
php artisan test --profile
```

<Warning>
  La mesure de couverture allonge significativement le temps d'exécution des tests. En CI, il est recommandé de la faire dans un job dédié ou uniquement lors des pull requests.
</Warning>

## Pages associées

<Card title="Introduction aux tests" icon="flask" href="/fr/testing">
  Découvrez la façon d'écrire des tests avec Laravel et l'utilisation de `php artisan test`.
</Card>


## Related topics

- [Se lancer dans les tests Laravel avec Pest PHP](/fr/blog/pest-introduction.md)
- [Gestion de la compatibilité de versions de package](/fr/advanced/package-versioning.md)
- [Introduction aux tests](/fr/testing.md)
- [Développer un package avec Testbench Workbench](/fr/advanced/package-workbench.md)
- [Tester des packages Laravel avec Orchestra Testbench](/fr/advanced/package-testing.md)
