Il problema è sempre lo stesso: il codice che hai appena scritto funziona in locale, ma in produzione si rompe perché non hai testato un caso limite. E quando il cliente chiama perché il checkout sparisce, ti ricordi che il tempo speso a scrivere test non è mai tempo perso.
Noi, di Meteora Web, lo vediamo ogni giorno. Abbiamo ereditato progetto con zero test e abbiamo passato notti a debuggare. Da lì abbiamo imparato: i test non sono un optional, sono la differenza tra un rilascio sicuro e un incidente. In questo articolo confrontiamo PHPUnit e Pest, due framework di testing per PHP e Laravel, con esempi reali tratti dalla nostra esperienza.
PHPUnit e Pest: Quali Sono le Differenze Principali?
PHPUnit è il framework storico, nato nel 2004. Pest è il nuovo arrivato (2019) che usa PHPUnit sotto il cofano ma cambia radicalmente la sintassi e l'esperienza di scrittura. Entrambi fanno la stessa cosa: eseguono asserzioni sulle tue classi. Ma il modo in cui li scrivi è diverso.
PHPUnit segue un approccio a classi: ogni test è un metodo dentro una classe che estende PHPUnit\Framework\TestCase. Pest usa funzioni globali e una sintassi fluida, ispirata a Jest (JavaScript). Pest è un wrapper di PHPUnit: puoi usare tutti i metodi di PHPUnit da Pest, ma scrivendo meno boilerplate.
Ecco un confronto visivo. Lo stesso test di una somma:
// PHPUnit
use PHPUnit\Framework\TestCase;
class MathTest extends TestCase
{
public function test_addition()
{
$result = 1 + 1;
$this->assertEquals(2, $result);
}
}
// Pest
test('addition')
->with(1, 1)
->assertTrue(2 === 1 + 1);
Nota subito: con Pest non devi creare una classe, non devi estendere nulla. Scrivi una funzione test() e incateni asserzioni. Per molti sviluppatori è più leggibile e veloce da scrivere.
Sponsored Protocol
Come Scrivere Test con PHPUnit in Laravel?
Laravel integra PHPUnit di default. I test si trovano in tests/Unit e tests/Feature. Ecco un esempio reale: testare che una rotta API restituisca un JSON con lo stato corretto.
// tests/Feature/UserApiTest.php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
class UserApiTest extends TestCase
{
use RefreshDatabase;
public function test_can_fetch_users()
{
User::factory()->count(3)->create();
$response = $this->getJson('/api/users');
$response->assertStatus(200)
->assertJsonCount(3)
->assertJsonStructure([
'*' => ['id', 'name', 'email']
]);
}
}
Qui usiamo RefreshDatabase per avere un database pulito a ogni test. assertJsonCount e assertJsonStructure sono metodi di Laravel che rendono i test espressivi.
Cosa fare subito: apri il tuo progetto Laravel, vai in tests/Feature e crea un test per una delle tue route. Esegui php artisan test per vederlo passare.
Sponsored Protocol
Come Scrivere Test con Pest in Laravel?
Pest si installa con Composer e diventa il runner di default se lo preferisci. Lo stesso test di prima in Pest diventa:
// tests/Feature/UserApiTest.php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('can fetch users via API', function () {
User::factory()->count(3)->create();
$response = $this->getJson('/api/users');
$response->assertStatus(200)
->assertJsonCount(3)
->assertJsonStructure([
'*' => ['id', 'name', 'email']
]);
});
La differenza principale è la funzione uses() per applicare i trait globalmente, e l'assenza di classe. Nota anche che $this esiste comunque grazie al binding automatico di Pest.
Pest offre anche funzioni di alto livello come beforeEach() e afterEach() per preparare il contesto, e gruppi di test con describe():
describe('User API', function () {
beforeEach(function () {
$this->user = User::factory()->create();
});
test('can list users', function () {
$response = $this->getJson('/api/users');
$response->assertOk();
});
test('can create a user', function () {
$response = $this->postJson('/api/users', [
'name' => 'Marco',
'email' => 'marco@example.com',
'password' => 'secret'
]);
$response->assertCreated();
});
});
Cosa fare subito: installa Pest con composer require pestphp/pest --dev, poi esegui php artisan pest:init. Converti un tuo test esistente in Pest per sentire la differenza.
Sponsored Protocol
PHPUnit o Pest: Quale Scegliere per il Tuo Progetto?
La scelta dipende dal team e dal contesto. Noi di Meteora Web usiamo entrambi. Su progetti Laravel nuovi preferiamo Pest per la velocità di scrittura e la leggibilità. Su codebase legacy con decine di test in PHPUnit, non lo migriamo: il tempo speso a riscrivere non vale il beneficio.
Ecco una checklist per decidere:
- Scegli PHPUnit se: hai un team abituato, molti test esistenti, o hai bisogno di compatibilità con tool CI che non riconoscono Pest (raro).
- Scegli Pest se: inizi un nuovo progetto, vuoi test più leggibili, o hai sviluppatori che arrivano da Jest/Mocha.
- Usa entrambi se: vuoi una transizione graduale. Pest può eseguire test PHPUnit senza problemi.
Ricorda: il framework conta meno della cultura del testing. Abbiamo visto aziende con PHPUnit perfetto e aziende con Pest disordinato. La differenza la fa la disciplina: testare prima di rilasciare, coprire i casi critici, mantenere i test verdi.
Un Esempio Reale di Test con Mocking e Database
Prendiamo un caso concreto: un servizio che calcola lo sconto su un ordine. Useremo mock per isolare le dipendenze.
Sponsored Protocol
// App\Services\DiscountService
class DiscountService
{
public function apply(Order $order, Coupon $coupon): float
{
if (! $coupon->isValid()) {
return $order->total;
}
return $order->total * (1 - $coupon->percentage);
}
}
// Test con PHPUnit
class DiscountServiceTest extends TestCase
{
public function test_applies_discount_when_coupon_valid()
{
$coupon = $this->createMock(Coupon::class);
$coupon->method('isValid')->willReturn(true);
$coupon->percentage = 0.2;
$order = new Order(['total' => 100]);
$service = new DiscountService();
$result = $service->apply($order, $coupon);
$this->assertEquals(80.0, $result);
}
}
// Test con Pest
test('applies discount when coupon valid', function () {
$coupon = Mockery::mock(Coupon::class);
$coupon->shouldReceive('isValid')->andReturn(true);
$coupon->percentage = 0.2;
$order = new Order(['total' => 100]);
$service = new DiscountService();
expect($service->apply($order, $coupon))->toBe(80.0);
});
Nota: in Pest usiamo lo stesso identico PHPUnit o possiamo usare Mockery (che ha una sintassi più fluida). Non c'è una risposta giusta; l'importante è testare.
Come Integrare PHPUnit e Pest nel Workflow CI
Entrambi si integrano con GitHub Actions, GitLab CI, o qualsiasi tool. Il comando è sempre php artisan test (che usa PHPUnit di default) o ./vendor/bin/pest se hai Pest come runner. Noi consigliamo di eseguire i test su ogni pull request e di bloccare il merge se falliscono.
Sponsored Protocol
# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
phpunit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer
- run: composer install --no-progress
- run: php artisan test
Cosa fare subito: aggiungi un workflow di test al tuo repository. Se non lo hai, parte da questo esempio.
Cosa Fare Adesso
- Scrivi un test per una funzione che usi oggi. Non serve un progetto intero: parti da una classe semplice (es. un helper per formattare date).
- Installa Pest su un progetto Laravel esistente e converti un test per capire se ti piace. Non serve migrare tutto.
- Aggiungi il workflow CI per eseguire i test automaticamente. È il modo più veloce per evitare regressioni.
- Leggi la documentazione ufficiale di PHPUnit e Pest per approfondire mocking e test di feature.
Noi, di Meteora Web, abbiamo visto progetti salvati da una suite di test solida. Se hai bisogno di una mano a impostare i test nel tuo progetto Laravel, parti dal nostro pillar sul testing.