Pest è 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
I test Pest vengono eseguiti da PHPUnit, quindi coesistono con i test PHPUnit esistenti. Esegui con php artisan test o vendor/bin/pest.
Definizione più semplice. Il nome del test è la sua descrizione.
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');});
Sintassi “it should…” in stile inglese, più vicina al linguaggio naturale.
it('reindirizza gli utenti non autenticati alla pagina di login', function () { $response = $this->get('/dashboard'); $response->assertRedirect('/login');});
expect($user)->toBeInstanceOf(User::class);expect($user->email)->toMatchRegex('/^.+@.+\..+$/');// verifica il salvataggio nel databaseexpect(User::where('email', '[email protected]')->exists())->toBeTrue();
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');
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],]);
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');});
Uno spy, a differenza di un mock, chiama l’implementazione reale registrando comunque le chiamate.
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');});
Mocka solo alcuni metodi, gli altri usano l’implementazione reale.
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();});
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();});
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.
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; });});
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();});
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');});
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.
Per la maggior parte dei progetti, RefreshDatabase è la scelta sicura. Valuta LazilyRefreshDatabase se la test suite è grande e molti test non usano il DB.
# mostra la coverage nel terminalephp artisan test --coverage# imposta una coverage minima (fallisce la CI se sotto soglia)php artisan test --coverage --min=80# genera un report HTMLvendor/bin/pest --coverage-html=coverage/# individua i test più lentiphp artisan test --profile
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.