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

# Test avanzati con Pest

> Guida sistematica all'uso avanzato di Pest — diventato default da Laravel 11 — con Expectation API, dataset, fake e Mockery.

## Cos'è Pest

[Pest](https://pestphp.com) è un framework di test costruito sopra PHPUnit; nei nuovi progetti Laravel 11 e successivi è adottato per default. Puoi riutilizzare tutte le assertion di PHPUnit scrivendo però test con una sintassi concisa basata su closure.

Differenze principali con PHPUnit:

| Aspetto              | Pest                                 | PHPUnit                  |
| -------------------- | ------------------------------------ | ------------------------ |
| Definizione dei test | Closure `test()` / `it()`            | Classi con metodi        |
| Assertion            | Expectation API (`expect()->toBe()`) | `$this->assert*()`       |
| Dataset              | `dataset()` / `with()`               | `@dataProvider`          |
| Hook                 | `beforeEach()` / `afterEach()`       | `setUp()` / `tearDown()` |
| Nesting              | Raggruppamento con `describe()`      | Separazione per classe   |

<Info>
  I test Pest vengono eseguiti da PHPUnit, quindi coesistono con i test PHPUnit esistenti. Esegui con `php artisan test` o `vendor/bin/pest`.
</Info>

## Quando usare `describe` / `it` / `test`

### `test()`

Definizione più semplice. Il nome del test è la sua descrizione.

```php theme={null}
test('l\'utente può fare login con la sua email', function () {
    $user = User::factory()->create();

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

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

### `it()`

Sintassi "it should..." in stile inglese, più vicina al linguaggio naturale.

```php theme={null}
it('reindirizza gli utenti non autenticati alla pagina di login', function () {
    $response = $this->get('/dashboard');

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

### `describe()`

Raggruppa test correlati. Utile per condividere `beforeEach()` e organizzare logicamente il file.

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

    it('può ottenere l\'elenco degli ordini', function () {
        Order::factory(3)->for($this->user)->create();

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

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

    it('non può ottenere gli ordini di altri utenti', function () {
        $other = User::factory()->create();
        Order::factory()->for($other)->create();

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

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

<Tip>
  `describe()` può essere annidato, ma nesting troppo profondi rendono i test difficili da leggere. Rimani su un massimo di 2 livelli.
</Tip>

## Expectation API

`expect()` è la sintassi di assertion specifica di Pest. Puoi concatenare le condizioni.

### Assertion di base

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

### Assertion su modelli

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

// verifica il salvataggio nel database
expect(User::where('email', 'test@example.com')->exists())->toBeTrue();
```

### Chain `and()`

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

### `each()` per validare ogni elemento di un array

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

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

## Test parametrici con i dataset

Per testare la stessa logica con più input usa `dataset()` o l'inline `with()`.

### Dataset inline

```php theme={null}
it('le email non valide restituiscono un errore di validazione', function (string $email) {
    $response = $this->post('/register', ['email' => $email]);

    $response->assertInvalid('email');
})->with([
    'testo semplice' => ['not-an-email'],
    'senza dominio' => ['user@'],
    'stringa vuota' => [''],
]);
```

### Dataset con nome

Definisci i dataset nella directory `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('le email non valide restituiscono un errore di validazione', function (string $email) {
    $response = $this->post('/register', ['email' => $email]);

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

### Dataset dinamico con closure

```php theme={null}
it('ogni piano utente ha un limite diverso', function (string $plan, int $limit) {
    $user = User::factory()->create(['plan' => $plan]);

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

## Test unitari con Mockery

### Mock di un servizio

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

test('il servizio di pagamento viene chiamato', 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');
});
```

### Spy

Uno spy, a differenza di un mock, chiama l'implementazione reale registrando comunque le chiamate.

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

test('verifica che il servizio di notifica sia stato chiamato', function () {
    $spy = $this->spy(NotificationService::class);

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

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

### Mock parziale

Mocka solo alcuni metodi, gli altri usano l'implementazione reale.

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

test('salta la scrittura del file di report', function () {
    $mock = $this->partialMock(ReportService::class, function (MockInterface $mock) {
        $mock->expects('writeFile')->andReturnNull();
    });

    $result = $mock->generate();

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

## Test di chiamate API esterne con HTTP fake

Con `Http::fake()` stubbi le risposte senza inviare richieste HTTP reali.

### Fake di base

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

test('ottiene le informazioni utente da un\'API esterna', 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');
});
```

### Test di risposte di errore

```php theme={null}
test('viene sollevata un\'eccezione quando l\'API restituisce un errore', function () {
    Http::fake([
        'api.example.com/*' => Http::response([], 503),
    ]);

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

### Simulazione di errori di rete

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

test('gestisce gli errori di connessione', function () {
    Http::fake(fn () => throw new ConnectionException('Connection refused'));

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

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

## Fake di eventi, email e notifiche

### `Event::fake()`

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

test('emette un evento quando viene creato un ordine', function () {
    Event::fake();

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

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

Puoi fakeare solo eventi specifici lasciando gli altri normali.

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

### `Mail::fake()`

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

test('invia una email di benvenuto alla registrazione', 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('invia una notifica alla spedizione', 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;
    });
});
```

## Test dei comandi Artisan

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

test('il comando per la pulizia dei dati funziona', 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();
});
```

### Test di comandi interattivi

```php theme={null}
test('un comando interattivo esegue dopo la conferma', function () {
    $this->artisan('reports:generate')
        ->expectsQuestion('Vuoi eseguire?', 'yes')
        ->expectsOutput('Report generato.')
        ->assertExitCode(0);
});
```

## `RefreshDatabase` e `LazilyRefreshDatabase`

### `RefreshDatabase`

Dopo ogni test fa rollback del database, mantenendolo pulito. All'avvio della test suite esegue la migrazione.

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

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

test('può creare un utente', function () {
    $user = User::factory()->create(['name' => 'Laravel']);

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

### `LazilyRefreshDatabase`

Con `RefreshDatabase` la migrazione viene verificata in tutti i test; con `LazilyRefreshDatabase` viene rimandata finché un test non modifica effettivamente il DB. Se hai molti test che non toccano il DB, velocizza la suite.

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

<Tip>
  Per la maggior parte dei progetti, `RefreshDatabase` è la scelta sicura. Valuta `LazilyRefreshDatabase` se la test suite è grande e molti test non usano il DB.
</Tip>

| Aspetto                    | `RefreshDatabase`     | `LazilyRefreshDatabase`          |
| -------------------------- | --------------------- | -------------------------------- |
| Momento della migrazione   | All'avvio della suite | Alla prima operazione DB         |
| Overhead sui test senza DB | Sì                    | No                               |
| Caso consigliato           | Caso generale         | Test numerosi con molti senza DB |

## Misurare la code coverage

Servono Xdebug o PCOV.

```bash theme={null}
# mostra la coverage nel terminale
php artisan test --coverage

# imposta una coverage minima (fallisce la CI se sotto soglia)
php artisan test --coverage --min=80

# genera un report HTML
vendor/bin/pest --coverage-html=coverage/

# individua i test più lenti
php artisan test --profile
```

<Warning>
  La misurazione della code coverage aumenta molto il tempo di esecuzione dei test. In CI consigliamo di separarla in un job dedicato o di eseguirla solo sulle pull request.
</Warning>

## Pagine correlate

<Card title="Introduzione ai test" icon="flask" href="/it/testing">
  Rivedi le basi dei test in Laravel e l'uso di `php artisan test`.
</Card>


## Related topics

- [Gestire la compatibilità tra versioni dei pacchetti](/it/advanced/package-versioning.md)
- [Iniziare a testare Laravel con Pest PHP](/it/blog/pest-introduction.md)
- [Sviluppo di pacchetti Laravel](/it/advanced/package-development.md)
- [Test da console](/it/console-tests.md)
- [Test del browser (Dusk)](/it/dusk.md)
