Laravel Testing nel 2026: Pest, Mocking e Domande da Colloquio Tecnico

Una guida completa al testing Laravel con Pest 4: unit test, feature test, strategie di mocking, test architetturali e domande frequenti nei colloqui tecnici.

Laravel Testing con Pest 4 e Mocking Best Practices 2026

Il panorama del testing in Laravel si è trasformato radicalmente con Pest 4, il mutation testing e i preset architetturali. Laravel 12 include il supporto nativo per Pest, rendendo l'esperienza di testing più espressiva e meno verbosa rispetto a PHPUnit puro. Questa guida copre i pattern essenziali sia per le applicazioni in produzione che per i colloqui tecnici.

Pest 4 è lo Standard

Pest 4.7 (basato su PHPUnit 12) è il framework di testing predefinito per Laravel 12. Richiede PHP 8.3+ e introduce mutation testing, sharding bilanciato nel tempo e preset architetturali pronti all'uso.

Configurare Pest 4 in un Progetto Laravel 12

Ogni nuova applicazione Laravel 12 viene scaffoldata automaticamente con Pest. Per i progetti esistenti, la migrazione da PHPUnit o Pest 3 richiede pochi minuti. La configurazione risiede in tests/Pest.php, dove vengono registrati i trait globali e i metodi helper.

tests/Pest.phpphp
use Illuminate\Foundation\Testing\RefreshDatabase;

pest()
    ->extend(Tests\TestCase::class)
    ->use(RefreshDatabase::class)
    ->in('Feature');

Questo singolo file sostituisce il vecchio trait CreatesApplication e l'ereditarietà della classe base di test. Il trait RefreshDatabase incapsula ogni test in una transazione del database, annullando automaticamente le modifiche.

tests/Feature/UserRegistrationTest.phpphp
use App\Models\User;

it('registers a new user with valid data', function () {
    $response = $this->postJson('/api/register', [
        'name' => 'Jane Doe',
        'email' => 'jane@example.com',
        'password' => 'SecurePass123!',
        'password_confirmation' => 'SecurePass123!',
    ]);

    $response->assertStatus(201)
        ->assertJsonStructure(['user' => ['id', 'name', 'email']]);

    expect(User::where('email', 'jane@example.com')->exists())->toBeTrue();
});

L'API expect() di Pest si integra perfettamente con le assertion di PHPUnit. La chiamata assertJsonStructure valida la struttura della risposta, mentre expect()->toBeTrue() conferma lo stato del database. Entrambi gli stili coesistono senza conflitti.

Unit Test vs Feature Test in Laravel

La distinzione tra unit test e feature test in Laravel determina quali parti del framework vengono avviate. Gli unit test vengono eseguiti senza il container dell'applicazione, risultando più veloci ma limitati alla logica pura. I feature test avviano l'applicazione completa, abilitando chiamate HTTP, query al database e risoluzione dei servizi.

tests/Unit/PriceCalculatorTest.phpphp
use App\Services\PriceCalculator;

describe('PriceCalculator', function () {
    it('applies a percentage discount correctly', function () {
        $calculator = new PriceCalculator();

        // 20% off a 150.00 base price
        $result = $calculator->applyDiscount(150.00, 20);

        expect($result)->toBe(120.00);
    });

    it('rejects negative discount values', function () {
        $calculator = new PriceCalculator();

        expect(fn () => $calculator->applyDiscount(100.00, -5))
            ->toThrow(InvalidArgumentException::class);
    });
});

Gli unit test puntano a classi isolate senza dipendenze esterne. Il blocco describe raggruppa le assertion correlate, e Pest 4 supporta blocchi describe annidati per gerarchie di test complesse.

I feature test dovrebbero costituire la maggior parte di una test suite Laravel. Rilevano bug di integrazione che gli unit test non individuano, come middleware di route errati, regole di validazione mancanti o relazioni Eloquent difettose. La regola generale è: se il codice interagisce con il database, il livello HTTP o una facade, occorre scrivere un feature test.

Strategie di Mocking con le Facade e Mockery

Il mocking nel testing Laravel isola il codice sotto test dalle dipendenze esterne. Le facade di Laravel forniscono implementazioni fake integrate per code, eventi, notifiche, mail e storage. Mockery gestisce tutto il resto.

tests/Feature/OrderProcessingTest.phpphp
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use App\Mail\OrderConfirmation;
use App\Jobs\ProcessPayment;

it('dispatches payment job and sends confirmation email', function () {
    // Fake both Mail and Queue facades
    Mail::fake();
    Queue::fake();

    $user = User::factory()->create();
    $order = Order::factory()->for($user)->create([
        'total' => 99.99,
        'status' => 'pending',
    ]);

    // Act: confirm the order via HTTP
    $this->actingAs($user)
        ->postJson("/api/orders/{$order->id}/confirm")
        ->assertOk();

    // Assert: payment job was dispatched with correct amount
    Queue::assertPushed(ProcessPayment::class, function ($job) use ($order) {
        return $job->order->id === $order->id
            && $job->order->total === 99.99;
    });

    // Assert: confirmation email was sent to the user
    Mail::assertSent(OrderConfirmation::class, function ($mail) use ($user) {
        return $mail->hasTo($user->email);
    });
});

I facade fake intercettano le chiamate a livello di framework, impedendo l'invio effettivo di email o l'accodamento di job. Le assertion basate su closure verificano i dati esatti passati a ciascun componente.

Mock ai Confini

Le classi di dominio interne non dovrebbero essere mockate. Il mocking va applicato ai confini del sistema: API di terze parti, mail, code, filesystem. Un mocking eccessivo rende i test fragili e strettamente accoppiati ai dettagli implementativi.

Per le dipendenze che non sono facade, l'iniezione avviene tramite costruttore con Mockery:

tests/Feature/PaymentGatewayTest.phpphp
use App\Services\PaymentGateway;
use App\Services\StripeClient;

it('charges the customer through the payment gateway', function () {
    // Create a mock of the Stripe client
    $stripeClient = Mockery::mock(StripeClient::class);
    $stripeClient->shouldReceive('charge')
        ->once()
        ->with('cus_abc123', 5000, 'usd')
        ->andReturn(['status' => 'succeeded', 'id' => 'ch_xyz']);

    // Bind the mock in the container
    $this->app->instance(StripeClient::class, $stripeClient);

    $gateway = app(PaymentGateway::class);
    $result = $gateway->processCharge('cus_abc123', 50.00);

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

Il binding del mock con $this->app->instance() sostituisce l'implementazione reale per la durata del test. Mockery verifica che charge sia stato chiamato esattamente una volta con gli argomenti attesi.

Test Architetturali con i Preset di Pest

Pest 4 include test architetturali che applicano regole strutturali all'intero codebase. Questi test vengono eseguiti sull'AST, non sul runtime, risultando estremamente veloci.

tests/Architecture/ArchitectureTest.phpphp
arch('controllers do not use Eloquent directly')
    ->expect('App\Http\Controllers')
    ->not->toUse('Illuminate\Database\Eloquent');

arch('services are final classes')
    ->expect('App\Services')
    ->toBeFinal();

arch('no debugging functions in production code')
    ->expect(['dd', 'dump', 'var_dump', 'ray'])
    ->not->toBeUsed();

La prima regola impedisce ai controller di accedere direttamente al database, imponendo un pattern a livello di servizio. La seconda garantisce che i servizi non possano essere estesi, riducendo la complessità dell'ereditarietà. La terza intercetta istruzioni di debug dimenticate prima che raggiungano la produzione.

Sono disponibili anche preset specifici per Laravel:

tests/Architecture/LaravelPresetTest.phpphp
arch()->preset()->laravel();
arch()->preset()->security();
arch()->preset()->php();

Queste tre righe applicano decine di regole: i model devono estendere la classe base corretta, i controller non devono contenere logica di business, funzioni insicure come eval() o md5() per l'hashing sono vietate, e le convenzioni PHP standard vengono rispettate.

Pronto a superare i tuoi colloqui su Laravel?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Mutation Testing per Verificare la Qualità dei Test

La code coverage misura quali righe vengono eseguite durante i test. Il mutation testing va oltre: modifica il codice sorgente e verifica se i test rilevano il cambiamento. Se una mutazione sopravvive, la test suite presenta una lacuna.

bash
# Run mutation testing on a specific class
php artisan test --mutate --class=App\\Services\\PriceCalculator

Pest 4 introduce mutazioni come il cambio di > in >=, la rimozione di istruzioni return e l'inversione di condizioni booleane. Un mutation score inferiore all'80% indica generalmente che i test verificano gli output senza controllare i casi limite.

php
// Example: this test has high coverage but low mutation score
it('calculates shipping cost', function () {
    $cost = calculateShipping(weight: 5.0, zone: 'domestic');

    // Only checks that the result is numeric
    expect($cost)->toBeFloat();
});

// Improved: catches mutations by asserting the exact value
it('calculates domestic shipping for 5kg package', function () {
    $cost = calculateShipping(weight: 5.0, zone: 'domestic');

    // Exact assertion catches operator and value mutations
    expect($cost)->toBe(12.50);
});

Il primo test passa anche se la logica di calcolo è completamente errata, purché restituisca un float. Il secondo test fallisce immediatamente quando una mutazione modifica la formula. Assertion specifiche producono mutation score più elevati.

Pattern di Testing del Database e Factory

Le model factory di Laravel generano dati di test realistici senza costruzione manuale di array. Pest 4 combinato con le factory produce un setup di dati leggibile e manutenibile.

tests/Feature/ArticlePublishingTest.phpphp
use App\Models\Article;
use App\Models\User;

it('publishes a draft article and updates the timestamp', function () {
    $author = User::factory()->create(['role' => 'editor']);

    $article = Article::factory()
        ->for($author, 'author')
        ->draft()
        ->create(['title' => 'Testing Best Practices']);

    $this->actingAs($author)
        ->patchJson("/api/articles/{$article->id}/publish")
        ->assertOk()
        ->assertJsonPath('data.status', 'published');

    $article->refresh();

    expect($article->status)->toBe('published')
        ->and($article->published_at)->not->toBeNull()
        ->and($article->published_at->isToday())->toBeTrue();
});

Lo state draft() della factory imposta valori predefiniti per gli articoli non pubblicati. Le assertion expect() concatenate con ->and() si leggono come linguaggio naturale e falliscono con messaggi descrittivi.

Testing Parallelo

Eseguendo php artisan test --parallel i test vengono distribuiti su più processi. Laravel crea automaticamente database di test separati per ogni processo, prevenendo conflitti tra i dati. Combinato con lo sharding bilanciato di Pest 4, si ottengono prestazioni CI ottimali.

Domande Frequenti sul Testing Laravel nei Colloqui

I colloqui tecnici per posizioni Laravel valutano frequentemente la conoscenza del testing. Ecco le domande più ricorrenti, insieme a ciò che una risposta efficace dovrebbe coprire.

Qual è la differenza tra fake(), mock() e spy() nel testing Laravel?

Facade fake() sostituisce l'intera facade con un'implementazione in memoria (Mail::fake, Queue::fake). mock() tramite Mockery imposta aspettative prima dell'esecuzione e fallisce se non vengono soddisfatte. spy() registra le interazioni e consente assertion dopo l'esecuzione, senza impostare aspettative preventive. La scelta dipende dalla necessità di verificare il comportamento (mock), registrare le interazioni (spy) o prevenire effetti collaterali (fake).

In che modo RefreshDatabase differisce da DatabaseTransactions?

RefreshDatabase esegue le migrazioni una volta e incapsula ogni test in una transazione. DatabaseTransactions presume che il database abbia già lo schema corretto e si limita a incapsulare i test in transazioni. RefreshDatabase è più sicuro per le pipeline CI dove il database potrebbe non esistere ancora. DatabaseTransactions è più veloce quando lo schema è garantito essere aggiornato.

Quando i feature test sono preferibili agli unit test?

I feature test dovrebbero coprire qualsiasi codice che interagisce con il livello HTTP, il database o i servizi Laravel. Gli unit test sono riservati a funzioni pure e value object senza dipendenze dal framework. In un'applicazione Laravel tipica, i feature test superano gli unit test in un rapporto di circa 3:1 o superiore. Questo riflette la realtà per cui la maggior parte del codice Laravel è intrinsecamente integrato con il framework.

Come il mutation testing migliora la qualità dei test oltre la code coverage?

La code coverage misura i percorsi di esecuzione. Un test può raggiungere il 100% di coverage chiamando ogni metodo senza effettuare assertion significative. Il mutation testing modifica il codice sorgente (cambia operatori, rimuove return, inverte booleani) e verifica che almeno un test fallisca. Le mutazioni sopravvissute rivelano assertion troppo permissive o del tutto assenti. php artisan test --mutate produce un mutation score percentuale insieme alla coverage standard.

Per ulteriori domande sui colloqui Laravel, la banca dati SharpSkill copre autenticazione, pattern del service container, relazioni Eloquent e architettura delle code.

Test delle Risposte HTTP e Assertion JSON

Gli helper di testing HTTP di Laravel verificano codici di stato, header, strutture JSON e destinazioni di redirect. Combinati con Pest, formano test di integrazione concisi.

tests/Feature/ApiAuthenticationTest.phpphp
use App\Models\User;

describe('API Authentication', function () {
    it('rejects unauthenticated requests with 401', function () {
        $this->getJson('/api/profile')
            ->assertUnauthorized();
    });

    it('returns the authenticated user profile', function () {
        $user = User::factory()->create([
            'name' => 'John Doe',
            'email' => 'john@example.com',
        ]);

        $this->actingAs($user)
            ->getJson('/api/profile')
            ->assertOk()
            ->assertJson([
                'data' => [
                    'name' => 'John Doe',
                    'email' => 'john@example.com',
                ],
            ]);
    });

    it('validates required fields on registration', function () {
        $this->postJson('/api/register', [])
            ->assertUnprocessable()
            ->assertJsonValidationErrors(['name', 'email', 'password']);
    });
});

Il blocco describe raggruppa i test relativi all'autenticazione. Ogni nome di test descrive il comportamento atteso, non l'implementazione. Il metodo assertJsonValidationErrors verifica che campi specifici contengano messaggi di errore di validazione.

Conclusione

  • Pest 4 con Laravel 12 elimina il boilerplate attraverso un'API di configurazione fluida, catene expect() e individuazione automatica dei test
  • I feature test dovrebbero costituire il nucleo di una test suite Laravel, con gli unit test riservati alla logica di business isolata
  • I facade fake (Mail::fake(), Queue::fake()) gestiscono il mocking a livello di framework, mentre Mockery gestisce le dipendenze di terze parti iniettate tramite il container
  • I test architetturali applicano regole strutturali (niente Eloquent nei controller, niente funzioni di debug) senza overhead a runtime
  • Il mutation testing con --mutate individua assertion deboli che la sola code coverage non rileva
  • I factory state e le assertion concatenate expect()->and() mantengono il setup dei dati di test leggibile e le assertion specifiche
  • Per la preparazione ai colloqui Laravel, la comprensione della distinzione mock/fake/spy, del comportamento di RefreshDatabase e del mutation testing dimostra una maturità nel testing che va oltre la semplice coverage

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Tag

#laravel
#testing
#pest
#php
#interview

Condividi

Articoli correlati