Laravel Testing met Pest 5 in 2026: TIA, Mocking en Interviewvragen

Best practices voor Laravel testing met Pest 5, Test Impact Analysis, Mockery, facade fakes en architectuurtests. Unit tests, feature tests, mocking-strategieen en veelgestelde interviewvragen voor Laravel-ontwikkelaars.

Laravel testing met het Pest PHP-framework, unit tests en mocking voor codekwaliteit

De best practices voor Laravel testing zijn flink verschoven met Pest 5, de TIA-engine (Test Impact Analysis) en PHPUnit 13. Laravel 13 levert eersteklas Pest-ondersteuning, waardoor de testervaring expressiever en minder uitgebreid wordt dan kale PHPUnit. Deze gids behandelt de patronen die ertoe doen voor zowel productietoepassingen als technische interviews.

Pest 5 Is de Standaard

Pest 5 (gebouwd op PHPUnit 13) is het standaard testframework voor Laravel 13. Het vereist PHP 8.4+ en introduceert de TIA-engine, first-party AI-agentverificatie, PHPStan-integratie en time-balanced sharding standaard.

Pest 5 Opzetten in een Laravel 13-project

Elke nieuwe Laravel 13-applicatie genereert Pest automatisch. Voor bestaande projecten duurt de migratie van Pest 4 een paar minuten, aangezien PHPUnit 13-compatibiliteit de belangrijkste overweging is. De configuratie staat in tests/Pest.php, waar globale traits en helper-methoden worden geregistreerd.

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

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

Dit ene bestand vervangt de oude CreatesApplication trait en de overerving van de basistestklasse. De RefreshDatabase trait verpakt elke test in een databasetransactie en draait de wijzigingen automatisch terug.

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();
});

De expect()-API van Pest schakelt natuurlijk in elkaar met PHPUnit-assertions. De assertJsonStructure-aanroep valideert de vorm van de response, terwijl expect()->toBeTrue() de databasestatus bevestigt. Beide stijlen bestaan zonder conflict naast elkaar.

Test Impact Analysis: Het Paradepaardje van Pest 5

De TIA-engine maakt Pest 5 transformatief voor grote codebases. Bij de eerste run registreert Pest welke tests welke bestanden raken. Elke volgende run voert alleen de tests uit die door wijzigingen worden beinvloed en speelt gecachte resultaten af voor al het andere.

bash
# First run: records baseline (requires PCOV or Xdebug)
php artisan test

# Subsequent runs: only affected tests execute
php artisan test

De testsuite van Laravel Cloud, met meer dan 19.000 tests, daalde van ongeveer drie minuten naar vijf seconden met TIA ingeschakeld. De engine herkent meer dan alleen PHP-bestanden: het detecteert wijzigingen in migrations, Blade-templates en gedeelde JavaScript-componenten.

TIA vereist een coverage driver (PCOV of Xdebug) om de baseline vast te leggen. Voor CI-pipelines blijven de gecachte mappings behouden tussen runs, waardoor de prestatiewinst cumulatief is.

Unit Tests vs Feature Tests in Laravel

Het onderscheid tussen unit- en feature-tests in Laravel bepaalt welke delen van het framework worden opgestart. Unit tests draaien zonder de applicatiecontainer, wat ze sneller maakt maar beperkt tot pure logica. Feature tests starten de volledige applicatie op, wat HTTP-aanroepen, databasequery's en service-resolutie mogelijk maakt.

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);
    });
});

Unit tests richten zich op geisoleerde klassen zonder externe afhankelijkheden. Het describe-blok groepeert gerelateerde assertions, en Pest 5 ondersteunt geneste describe-blokken voor complexe testhierarchien.

Feature tests zouden het merendeel van een Laravel-testsuite moeten vormen. Ze vangen integratiebugs op die unit tests missen, zoals onjuiste route-middleware, ontbrekende validatieregels of kapotte Eloquent-relaties. Een veelgebruikte vuistregel: als de code de database, de HTTP-laag of een facade raakt, schrijf dan een feature test.

Mocking-strategieen met Facades en Mockery

Mocking in Laravel testing isoleert de code die wordt getest van externe afhankelijkheden. Laravel-facades bieden ingebouwde fake-implementaties voor queues, events, notificaties, mail en storage. Mockery regelt al het overige.

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);
    });
});

Facade fakes onderscheppen aanroepen op frameworkniveau, zodat er geen echte e-mails worden verstuurd of jobs worden verzonden. De op closures gebaseerde assertions verifieren de exacte data die aan elke component wordt doorgegeven.

Mock op Grenzen

Vermijd het mocken van interne domeinklassen. Mock op de grens: third-party API's, mail, queues, bestandssystemen. Te veel mocken maakt tests broos en sterk gekoppeld aan implementatiedetails.

Voor afhankelijkheden die geen facades zijn, injecteer ze via de constructor en gebruik 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');
});

Het binden van de mock met $this->app->instance() vervangt de echte implementatie voor de duur van de test. Mockery verifieert dat charge precies een keer is aangeroepen met de verwachte argumenten.

Architectuurtests met Pest-presets

Pest 5 bevat architectuurtests die structurele regels in de hele codebase afdwingen. Deze tests draaien tegen de AST, niet tegen de runtime, waardoor ze extreem snel zijn.

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();

De eerste regel voorkomt dat controllers de database rechtstreeks bevragen en dwingt zo een servicelaag-patroon af. De tweede zorgt ervoor dat services niet uitgebreid kunnen worden, wat de overervingscomplexiteit vermindert. De derde vangt achtergebleven debug-statements op voordat ze de productie bereiken.

Laravel-specifieke presets zijn ook beschikbaar:

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

Deze drie regels dwingen tientallen regels af: modellen erven de juiste basisklasse, controllers bevatten geen bedrijfslogica, geen onveilige functies zoals eval() of md5() voor hashing, en standaard PHP-conventies worden gevolgd.

Klaar om je Laravel gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Mutatietests om Testkwaliteit te Verifieren

Code coverage meet welke regels tijdens tests worden uitgevoerd. Mutatietests gaat verder: het wijzigt de broncode en controleert of de tests de verandering detecteren. Als een mutatie overleeft, heeft de testsuite een gat.

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

Pest 5 introduceert mutaties zoals het veranderen van > naar >=, het verwijderen van return-statements en het omkeren van booleaanse condities. Een mutatiescore onder de 80% wijst meestal op tests die uitvoer verifieren zonder edge cases te controleren.

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);
});

De eerste test slaagt zelfs als de berekeningslogica volledig fout is, zolang er maar een float wordt geretourneerd. De tweede test faalt onmiddellijk wanneer een mutatie de formule verandert. Specifieke assertions leveren hogere mutatiescores op.

Databasetestpatronen en Factories

Laravel-modelfactories genereren realistische testdata zonder handmatige array-constructie. Pest 5 in combinatie met factories levert leesbare, onderhoudbare data-setup op.

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();
});

De draft() factory-state stelt standaardwaarden in voor ongepubliceerde artikelen. Geketende expect()-assertions met ->and() lezen als natuurlijke taal en falen met beschrijvende meldingen.

Parallel Testen en Sharding

Voer php artisan test --parallel uit om tests over meerdere processen uit te voeren. Laravel maakt automatisch aparte testdatabases voor elk proces, wat dataconflicten voorkomt. De time-balanced sharding van Pest 5 verdeelt tests op basis van werkelijke uitvoeringstijd in plaats van aantal, zodat elke CI-shard op hetzelfde moment klaar is.

Veelgestelde Interviewvragen over Laravel Testing

Technische interviews voor Laravel-functies peilen regelmatig naar testkennis. Hieronder de vragen die het vaakst voorkomen, samen met wat een sterk antwoord behandelt.

Wat is het verschil tussen fake(), mock() en spy() in Laravel testing?

Facade fake() vervangt de hele facade door een in-memory implementatie (Mail::fake, Queue::fake). mock() via Mockery stelt verwachtingen in voor de uitvoering en faalt als ze niet worden ingelost. spy() registreert interacties en staat assertions toe na de uitvoering, zonder vooraf verwachtingen in te stellen. De keuze hangt ervan af of de test gedrag moet verifieren (mock), interacties moet registreren (spy) of neveneffecten moet voorkomen (fake).

Hoe verschilt RefreshDatabase van DatabaseTransactions?

RefreshDatabase voert migraties een keer uit en verpakt elke test in een transactie. DatabaseTransactions gaat ervan uit dat de database al het juiste schema heeft en verpakt tests alleen in transacties. RefreshDatabase is veiliger voor CI-pipelines waar de database mogelijk nog niet bestaat. DatabaseTransactions is sneller wanneer het schema gegarandeerd up-to-date is.

Wanneer verdienen feature tests de voorkeur boven unit tests?

Feature tests zouden alle code moeten dekken die interageert met de HTTP-laag, de database of Laravel-services. Unit tests zijn voorbehouden aan pure functies en waardeobjecten zonder frameworkafhankelijkheden. In een typische Laravel-applicatie overtreffen feature tests de unit tests in een verhouding van ongeveer 3:1 of hoger. Dit weerspiegelt de realiteit dat de meeste Laravel-code inherent geintegreerd is met het framework.

Hoe verbetert Test Impact Analysis de CI-performance?

TIA registreert bij de eerste run welke tests welke bronbestanden raken. Bij volgende runs voert Pest alleen de tests uit die door gewijzigde bestanden worden beinvloed en speelt gecachte resultaten af voor ongewijzigde tests. Grote suites die minuten duurden kunnen in seconden worden voltooid. De afweging: TIA vereist een coverage driver (PCOV of Xdebug) om de dependency map te bouwen, en de eerste run duurt langer dan een normale run.

Hoe verbetert mutatietesten de testkwaliteit verder dan code coverage?

Code coverage meet uitvoeringspaden. Een test kan 100% coverage behalen door elke methode aan te roepen zonder iets zinnigs te asserten. Mutatietesten wijzigt de broncode (operators veranderen, returns verwijderen, booleans omkeren) en verifieert dat ten minste een test faalt. Overlevende mutaties onthullen assertions die te los of volledig afwezig zijn. Het uitvoeren van php artisan test --mutate levert een mutatiescore-percentage op naast de standaard coverage.

Voor meer Laravel-interviewvragen behandelt de SharpSkill-vragenbank authenticatie, service container-patronen, Eloquent-relaties en queue-architectuur.

HTTP-responses en JSON-assertions Testen

De HTTP-testhelpers van Laravel verifieren statuscodes, headers, JSON-structuren en redirect-doelen. In combinatie met Pest vormen ze beknopte integratietests.

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']);
    });
});

Het describe-blok groepeert authenticatiegerelateerde tests. Elke testnaam beschrijft het verwachte gedrag, niet de implementatie. De assertJsonValidationErrors-methode controleert of specifieke velden validatiefoutmeldingen hebben.

Sources

Belangrijkste Punten over Laravel Testing met Pest 5

  • Pest 5 met Laravel 13 elimineert boilerplate via een vloeiende configuratie-API, expect()-ketens en automatische testdetectie
  • De TIA-engine voert alleen beinvloede tests opnieuw uit, waardoor de uitvoeringstijd van grote suites van minuten naar seconden gaat zonder coverage-nauwkeurigheid op te offeren
  • Feature tests zouden de kern van een Laravel-testsuite moeten vormen, met unit tests voorbehouden aan geisoleerde bedrijfslogica
  • Facade fakes (Mail::fake(), Queue::fake()) handelen mocking op frameworkniveau af, terwijl Mockery third-party afhankelijkheden afhandelt die via de container worden geinjecteerd
  • Architectuurtests dwingen structurele regels af (geen Eloquent in controllers, geen debug-functies) zonder runtime-overhead
  • Mutatietests met --mutate vangt zwakke assertions op die code coverage alleen mist
  • Factory-states en geketende expect()->and()-assertions houden de testdata-setup leesbaar en de assertions specifiek
  • Voor Laravel-interviewvoorbereiding toont begrip van mock/fake/spy, het gedrag van RefreshDatabase, TIA en mutation testing een testvolwassenheid die verder gaat dan basale coverage

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Dagelijkse challenge

Zie jij de bug in Laravel?

Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Anthony Fillion-Maillet

Geschreven door

Anthony Fillion-Maillet

Oprichter van SharpSkill

Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.

Bijgewerkt op 2 september 2026

Tags

#laravel
#testing
#pest
#php
#mocking
#best-practices
#interview

Delen

Gerelateerde artikelen