Tests Laravel en 2026 : Pest, Mocking et Questions d'Entretien Technique

Guide complet des tests Laravel avec Pest en 2026 : tests unitaires, tests fonctionnels, mocking de facades, tests d'architecture, mutation testing et questions d'entretien technique pour developpeurs PHP.

Guide technique sur les tests Laravel avec Pest couvrant le mocking, les tests d'architecture et la preparation aux entretiens techniques 2026

La qualite logicielle en PHP repose desormais autant sur la robustesse des tests que sur l'elegance du code applicatif. Dans l'ecosysteme Laravel, Pest s'est impose comme le framework de test de reference, remplacant progressivement PHPUnit brut dans la majorite des projets professionnels. En 2026, la maitrise de Pest, du mocking de facades et des tests d'architecture constitue un prerequis technique incontournable lors des processus de recrutement. Les equipes qui deploient Laravel en production attendent des candidats qu'ils sachent ecrire des tests expressifs, maintenir une couverture pertinente et structurer leurs suites de tests de maniere professionnelle.

Cet article analyse en profondeur les pratiques de testing Laravel avec Pest, depuis la configuration initiale jusqu'aux tests mutationnels, en passant par le mocking avance et les tests d'architecture. Chaque section correspond a une competence directement evaluable en entretien technique, accompagnee d'exemples de code immediatement transposables dans un exercice pratique ou un projet reel.

Pourquoi Pest plutot que PHPUnit brut ?

Pest repose sur PHPUnit mais propose une syntaxe expressive inspiree de Jest et RSpec. La chaine expect()->toBe() remplace les assertions classiques assertEquals(), les blocs describe/it structurent les tests de maniere lisible, et les fonctionnalites natives comme les tests d'architecture et le mutation testing reduisent la dependance aux packages tiers. Pest reste entierement compatible avec PHPUnit : tout test PHPUnit existant fonctionne sans modification dans une suite Pest.

Configurer Pest dans un Projet Laravel

La configuration de Pest dans un projet Laravel s'articule autour du fichier tests/Pest.php, qui definit les traits et les classes de base appliques automatiquement a chaque fichier de test. Cette centralisation evite la repetition de declarations communes et garantit la coherence de l'environnement de test a travers l'ensemble de la suite.

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

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

La methode extend() associe la classe TestCase de Laravel a tous les tests, fournissant l'acces aux methodes HTTP (getJson, postJson), a l'authentification simulee (actingAs) et aux assertions de reponse. Le trait RefreshDatabase reinitialise la base de donnees entre chaque test via des transactions, ce qui garantit l'isolement complet sans le cout d'un rechargement des migrations a chaque execution.

La directive ->in('Feature') limite l'application de cette configuration aux tests fonctionnels. Les tests unitaires situes dans tests/Unit/ heritent d'un environnement plus leger, sans connexion a la base de donnees, ce qui accelere leur execution. Cette separation architecturale entre tests unitaires et fonctionnels reflecte la pyramide de tests classique et constitue un point d'evaluation courant en entretien.

Tests Fonctionnels : Valider les Endpoints HTTP

Les tests fonctionnels dans Laravel verifient le comportement complet d'un endpoint, de la requete HTTP entrante jusqu'a la reponse JSON ou la redirection. Pest simplifie l'ecriture de ces tests grace a une syntaxe fluide qui permet d'enchainer assertions de statut, assertions de structure JSON et verifications en base de donnees.

Le test suivant valide l'inscription d'un utilisateur via une API REST.

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

Plusieurs elements meritent une attention particuliere. La methode postJson() envoie une requete POST avec les en-tetes JSON automatiquement configures, simulant un client API reel. L'assertion assertJsonStructure valide la presence des cles attendues sans imposer de valeurs specifiques, ce qui rend le test resilient aux modifications mineures de la reponse. L'assertion finale via expect() confirme que l'utilisateur a bien ete persiste en base de donnees, verifiant ainsi l'ensemble de la chaine de traitement.

Cette approche de test dite "de bout en bout au niveau HTTP" constitue le standard en Laravel. Elle valide simultanement le routage, le middleware, la validation des donnees, la logique du controleur et la persistance. Un echec dans n'importe quelle couche se manifeste dans le test, offrant un filet de securite complet pour les refactorisations.

Tests Unitaires avec Pest : Logique Metier Isolee

Les tests unitaires ciblent des classes ou des methodes individuelles, sans interaction avec la base de donnees, le systeme de fichiers ou les services externes. Ils s'executent en quelques millisecondes et constituent la base la plus fiable de la pyramide de tests. Le bloc describe/it de Pest structure ces tests de maniere lisible.

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

Le bloc describe regroupe les tests lies a une meme classe ou fonctionnalite, facilitant la lecture du rapport d'execution. L'assertion toThrow() valide qu'une exception est correctement levee pour des entrees invalides, un pattern defensif essentiel dans la logique metier. Le passage d'une closure a expect() permet de capturer l'exception sans interrompre le test.

Lors d'un entretien technique, la capacite a distinguer ce qui releve du test unitaire (logique de calcul, transformation de donnees, validation de regles metier) de ce qui releve du test fonctionnel (interaction HTTP, integration avec la base de donnees) constitue un indicateur cle de maturite technique.

Mocking de Facades Laravel : Mail, Queue et Notifications

Laravel fournit un systeme de facades qui simplifie l'acces aux services du framework. Pour les tests, ces facades offrent des methodes fake() qui remplacent l'implementation reelle par un double de test capable d'enregistrer les appels recus sans produire d'effets de bord. Ce mecanisme est fondamental pour tester les processus qui envoient des emails, dispatchent des jobs ou declenchent des notifications.

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

L'appel a Mail::fake() et Queue::fake() intercepte respectivement les envois d'emails et le dispatch de jobs pour toute la duree du test. Aucun email reel n'est envoye, aucun job n'est execute en arriere-plan. Les assertions assertPushed et assertSent verifient ensuite que les bonnes classes ont ete dispatchees avec les parametres attendus.

La closure passee a Queue::assertPushed illustre un pattern avance : elle permet de verifier non seulement que le job a ete dispatche, mais aussi que ses proprietes contiennent les valeurs correctes. Cette granularite d'assertion detecte les bugs subtils ou le mauvais objet serait passe au job, erreur courante lors des refactorisations.

Ce type de test est systematiquement pose en entretien technique Laravel. La reponse attendue demontre la comprehension du fait que les facades sont des proxies vers des services resolubles via le conteneur d'injection de dependances, et que le mode fake() remplace temporairement le service reel par un espion de test.

Mocking Avance avec Mockery : Services Externes

Pour les services qui ne sont pas des facades Laravel, Mockery offre un controle granulaire sur le comportement des doubles de test. Ce cas se presente frequemment avec les clients de services tiers comme Stripe, Twilio ou des API internes.

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

La methode Mockery::mock() cree un double de test qui intercepte les appels de methodes. L'enchainement shouldReceive()->once()->with()->andReturn() definit un contrat strict : la methode charge doit etre appelee exactement une fois, avec les parametres specifies, et retournera la valeur definie. Tout ecart provoque un echec du test.

L'instruction $this->app->instance() enregistre le mock dans le conteneur de services de Laravel. Lorsque PaymentGateway demande une instance de StripeClient par injection de dependances, le conteneur fournit le mock au lieu de l'implementation reelle. Ce mecanisme repose sur le principe d'inversion de dependances et constitue la cle de voute de la testabilite dans les applications Laravel bien architecturees.

La distinction entre le montant passe a processCharge (50.00 en dollars) et celui passe a charge (5000 en centimes) revele une logique de conversion dans le PaymentGateway. Le test valide implicitement cette transformation, detectant toute regression dans le calcul sans avoir a inspecter directement le code de la classe.

Prêt à réussir tes entretiens Laravel ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Tests d'Architecture : Imposer les Conventions du Projet

Pest introduit une fonctionnalite unique dans l'ecosysteme PHP : les tests d'architecture. Ces tests verifient que le code source respecte des conventions structurelles definies par l'equipe, sans executer la logique applicative. Les violations sont detectees statiquement, a la maniere d'un linter avance.

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

Le premier test impose une separation architecturale stricte : les controleurs ne doivent pas manipuler Eloquent directement, forcant l'utilisation d'une couche service ou repository. Cette contrainte previent l'accumulation de logique metier dans les controleurs, anti-pattern classique des applications Laravel mal structurees.

Le deuxieme test garantit que les classes de service sont declarees final, empechant l'heritage sauvage qui complexifie la maintenance. Le troisieme detecte les fonctions de debogage oubliees dans le code de production, probleme qui provoque des fuites d'information en environnement de production.

Pest propose egalement des presets d'architecture preconfigures qui couvrent les conventions standard de Laravel.

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

Le preset laravel() verifie que les controleurs etendent la bonne classe de base, que les modeles se trouvent dans le bon namespace, et que les conventions de nommage sont respectees. Le preset security() detecte l'utilisation de fonctions dangereuses comme eval(), exec() ou shell_exec(). Le preset php() impose les bonnes pratiques generales du langage.

En entretien, la connaissance des tests d'architecture distingue les candidats qui ont une vision globale de la qualite logicielle de ceux qui se limitent aux tests fonctionnels. Ces tests codifient les decisions architecturales de l'equipe et les rendent executables automatiquement dans le pipeline de CI.

Mutation Testing : Evaluer la Qualite Reelle des Tests

La couverture de code mesure quelles lignes sont executees pendant les tests, mais ne dit rien sur la qualite des assertions. Le mutation testing comble cette lacune en modifiant le code source de maniere systematique (remplacement d'operateurs, suppression de conditions, modification de valeurs de retour) et en verifiant que les tests detectent chaque mutation. Un test qui passe malgre une mutation est un test insuffisamment precis.

Pest integre nativement le mutation testing depuis la version 3, rendant cette pratique accessible sans configuration supplementaire.

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

La commande analyse la classe ciblee, genere des mutants et execute la suite de tests pour chacun d'entre eux. Le rapport indique le score de mutation : le pourcentage de mutants detectes par les tests existants.

L'exemple suivant illustre la difference entre un test a forte couverture mais faible score de mutation et un test veritablement robuste.

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

Le premier test passe meme si la formule de calcul est completement modifiee : n'importe quel nombre flottant satisfait l'assertion toBeFloat(). Le second test impose la valeur exacte attendue, detectant toute mutation dans la logique de calcul. Cette distinction entre assertions larges et assertions precises represente un concept fondamental que les recruteurs evaluent regulierement.

Le mutation testing revele les zones du code ou la couverture quantitative masque une faiblesse qualitative. Un score de mutation de 90% ou plus indique une suite de tests veritablement fiable, capable de detecter les regressions subtiles qu'une couverture de lignes a 100% laisserait passer.

Scenarios de Tests Complets pour l'Entretien

Les entretiens techniques Laravel proposent frequemment des exercices de test portant sur des scenarios realistes : publication d'article, authentification API, validation de formulaires. Les exemples suivants couvrent ces cas d'usage courants.

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

Ce test valide un workflow complet de publication. La factory draft() cree un article a l'etat brouillon, simulant le contexte initial. L'assertion assertJsonPath verifie une valeur precise dans la reponse JSON sans imposer la structure entiere. L'appel a refresh() recharge le modele depuis la base de donnees, garantissant que les assertions portent sur l'etat persiste et non sur un cache local.

La chaine d'assertions Pest avec ->and() permet de verifier plusieurs proprietes dans une seule expression fluide, ameliorant la lisibilite du test. La verification isToday() utilise les capacites de Carbon integrees a Laravel pour valider que le timestamp de publication correspond au jour courant.

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

Ce groupe de tests couvre les trois scenarios fondamentaux de toute API authentifiee : le rejet des requetes non authentifiees, la reponse correcte pour un utilisateur connecte, et la validation des champs obligatoires. L'assertion assertUnauthorized() verifie le statut 401, tandis que assertUnprocessable() correspond au statut 422 standard pour les erreurs de validation.

L'utilisation de assertJsonValidationErrors valide que les erreurs de validation sont correctement formatees et associees aux champs concernes. Ce pattern est indispensable pour les API consommees par des clients frontend qui affichent les messages d'erreur sous chaque champ de formulaire.

Questions d'Entretien Technique Laravel Testing

Les questions de testing en entretien Laravel evaluent la comprehension des mecanismes sous-jacents, pas la simple memorisation de la syntaxe. Les points suivants couvrent les sujets les plus frequemment abordes.

Quelle est la difference entre RefreshDatabase et DatabaseMigrations ? Le trait RefreshDatabase utilise des transactions de base de donnees pour annuler les changements apres chaque test, ce qui est significativement plus rapide. DatabaseMigrations execute les migrations completes avant chaque test et tronque les tables apres, ce qui est plus lent mais necessaire dans certains cas ou les transactions ne suffisent pas (par exemple avec des tests impliquant plusieurs connexions de base de donnees).

Pourquoi utiliser les factories plutot que des insertions manuelles ? Les factories encapsulent la creation d'objets de test avec des valeurs par defaut coherentes. Elles permettent de ne specifier que les attributs pertinents pour le test en cours, rendant l'intention du test explicite. Les factories gerernt automatiquement les relations entre modeles via les methodes for() et has(), eliminant le code repetitif de creation de donnees.

Comment tester un middleware sans tester tout le controleur ? Laravel permet d'instancier un middleware directement et d'appeler sa methode handle() avec une requete fabriquee. Cette approche isole la logique du middleware de celle du controleur. Alternativement, un test fonctionnel cible avec des assertions sur le statut HTTP permet de verifier le comportement du middleware en contexte.

Quand faut-il utiliser Mockery plutot que les fakes de facades ? Les fakes de facades (Mail::fake(), Queue::fake()) suffisent pour les services integres a Laravel. Mockery est necessaire pour les services tiers (clients API, SDK externes) qui ne sont pas des facades. La regle generale : utiliser les fakes Laravel quand ils existent, Mockery pour le reste.

Que valide un test d'architecture que les tests classiques ne couvrent pas ? Les tests d'architecture verifient la structure et les dependances du code sans l'executer. Ils detectent les violations de conventions (un controleur qui utilise Eloquent directement, une fonction de debug oubliee, un service non declare final) que les tests fonctionnels ne peuvent pas capter car le code fonctionne correctement malgre la violation architecturale.

Comment interpreter un score de mutation bas malgre une couverture de code elevee ? Un score de mutation bas avec une couverture elevee signifie que les tests executent le code mais ne verifient pas ses resultats avec precision. La solution consiste a remplacer les assertions larges (toBeFloat, toBeArray) par des assertions precises (toBe(12.50), toHaveCount(3)) qui detectent les modifications de la logique metier.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Conclusion

Le testing Laravel avec Pest en 2026 represente bien plus qu'une competence technique supplementaire : il constitue un pilier de la credibilite professionnelle d'un developpeur PHP. La capacite a ecrire des tests expressifs, a mocker correctement les dependances externes et a maintenir des conventions architecturales automatisees distingue les profils seniors des developpeurs intermediaires.

Les points essentiels a retenir pour les entretiens techniques :

  • Configuration Pest : le fichier tests/Pest.php centralise les traits et classes de base, separant clairement tests unitaires et fonctionnels
  • Tests fonctionnels : la combinaison postJson / assertStatus / expect couvre l'ensemble de la chaine de traitement d'un endpoint
  • Mocking de facades : Mail::fake(), Queue::fake() et les assertions associees constituent le pattern standard pour tester les processus asynchrones
  • Mockery : pour les services tiers, l'injection du mock via le conteneur de services respecte le principe d'inversion de dependances
  • Tests d'architecture : ils codifient les decisions structurelles de l'equipe et les rendent executables dans le pipeline CI/CD
  • Mutation testing : le score de mutation revele la qualite reelle des assertions, au-dela de la simple couverture de lignes
  • Factories et assertions : la maitrise des factories Laravel et des assertions JSON granulaires accelere l'ecriture de tests maintenables

Dans un processus de recrutement Laravel, la demonstration d'une approche structuree du testing transforme un exercice technique en vitrine de competences professionnelles. Chaque test bien ecrit temoigne d'une comprehension profonde du framework et d'une rigueur qui rassure les equipes techniques sur la fiabilite du code a venir.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#laravel
#testing
#pest
#php
#interview

Partager

Articles similaires