Développement Web

Tester son application Laravel 11 avec Pest : tutoriel pas à pas

12 min de lecture

📍 Guide de référence de cette série : Laravel 11 et PHP 8.4 : installer l’environnement et maîtriser la nouvelle architecture

Une application sans tests est une application dont vous ne pouvez pas modifier le code avec confiance. Laravel 11 inclut Pest par défaut dans l’installeur interactif, signalant que l’écosystème PHP a adopté ce framework de test comme standard de facto. Note (mai 2026) : Pest 4 vient d’être publié et apporte le browser testing comme fonctionnalité phare ; les commandes d’installation et les concepts couverts dans ce tutoriel (mutation testing, arch presets, datasets) restent valides en Pest 3 comme en Pest 4. Si vous démarrez un projet en 2026, viser Pest 4. Pest 3 apporte une syntaxe expressive héritée de Jest et RSpec, des rapports lisibles en couleur, le mutation testing pour évaluer la qualité de votre suite, et les arch presets pour valider l’architecture de votre code. Ce tutoriel couvre l’installation de Pest 3 dans un projet Laravel 11, les tests unitaires, les tests fonctionnels HTTP avec assertions, l’authentification dans les tests avec Sanctum, et les arch presets pour Laravel.

Prérequis

Étape 1 — Installer Pest 3 dans un projet Laravel 11 existant

Si vous avez créé votre projet avec laravel new et choisi Pest dans le wizard interactif, Pest est déjà installé. Sinon, voici comment l’ajouter à un projet existant. L’installation de Pest remplace les fichiers de test PHPUnit par défaut mais garde la compatibilité totale — Pest s’exécute sur PHPUnit en coulisse.

# Installer Pest 3 et le plugin Laravel
composer require pestphp/pest pestphp/pest-plugin-laravel --dev --with-all-dependencies

# Initialiser Pest : génère Pest.php, met à jour phpunit.xml
./vendor/bin/pest --init

La commande pest:install crée deux fichiers importants : tests/Pest.php (configuration globale des tests, helpers et datasets) et adapte phpunit.xml pour utiliser Pest comme runner. Elle crée aussi les dossiers tests/Unit/ et tests/Feature/ si absents. Vérifiez que l’installation est fonctionnelle en lançant la suite vide :

./vendor/bin/pest

La sortie attendue : un rapport vert avec Tests: 2 passed (les deux tests d’exemple créés par défaut). Si vous voyez une erreur de classe non trouvée, vérifiez que composer dump-autoload a été exécuté après l’installation.

Étape 2 — Comprendre la syntaxe Pest : it(), test() et expect()

Pest remplace la verbosité de PHPUnit par une syntaxe fonctionnelle. Au lieu de classes qui étendent TestCase avec des méthodes préfixées test_, vous écrivez des fonctions it() ou test() avec des closures. Voici la correspondance directe entre les deux styles :

<?php
// Syntaxe PHPUnit
class ArticleTest extends TestCase
{
    public function test_article_has_a_title(): void
    {
        $article = new Article(['title' => 'Mon article']);
        $this->assertEquals('Mon article', $article->title);
    }
}

// Syntaxe Pest — équivalent exact
it('has a title', function () {
    $article = new Article(['title' => 'Mon article']);
    expect($article->title)->toBe('Mon article');
});

// Syntaxe Pest avec test() — pour les descriptions plus longues
test('un article sans titre est invalide', function () {
    $article = Article::factory()->make(['title' => '']);
    expect($article->title)->toBeEmpty();
});

La fonction expect() est le cœur de Pest : elle wrape une valeur et expose une API chaînable de matchers (toBe(), toEqual(), toBeNull(), toContain(), toHaveCount(), toBeInstanceOf(), etc.). Le résultat des assertions est affiché avec le nom du test en couleur — vert pour les succès, rouge pour les échecs avec le message explicatif et la ligne du fichier.

Étape 3 — Tests unitaires : tester les modèles et les services

Les tests unitaires valident des unités de code isolées — une méthode de modèle, un service, une règle de validation — sans faire appel à la base de données ni au serveur HTTP. Dans Pest, un test unitaire est un test dans le dossier tests/Unit/ qui n’étend pas RefreshDatabase. Créez un premier test unitaire pour le modèle Article :

php artisan pest:test Unit/ArticleTest --unit
<?php
// tests/Unit/ArticleTest.php

use AppModelsArticle;
use IlluminateSupportStr;

it('génère un slug à partir du titre', function () {
    $article = Article::factory()->make(['title' => 'Mon Premier Article 2025']);

    expect($article->slug)->toBe('mon-premier-article-2025');
});

it('est en statut draft par défaut', function () {
    $article = new Article();

    expect($article->status)->toBe('draft');
});

it('date de publication est une instance Carbon quand castée', function () {
    $article = Article::factory()->make([
        'published_at' => '2026-01-15 10:00:00',
    ]);

    expect($article->published_at)->toBeInstanceOf(CarbonCarbon::class);
});

// Dataset : tester plusieurs slugs en une seule déclaration
it('slugifie correctement les titres accentués', function (string $title, string $expectedSlug) {
    $article = Article::factory()->make(['title' => $title]);
    expect($article->slug)->toBe($expectedSlug);
})->with([
    'titre avec accents'         => ['Hébergement VPS à Dakar', 'hebergement-vps-a-dakar'],
    'titre avec caractères spec' => ['API REST : guide 2025', 'api-rest-guide-2025'],
    'titre en majuscules'        => ['NODEJS AVANCÉ', 'nodejs-avance'],
]);

La fonctionnalité with() (datasets) est l’une des plus puissantes de Pest : elle exécute le même test avec plusieurs jeux de données sans dupliquer le corps du test. Chaque entrée du dataset génère un test distinct dans le rapport, avec son propre nom. Lancez uniquement les tests unitaires avec :

./vendor/bin/pest --filter=Unit

Étape 4 — Tests fonctionnels HTTP avec RefreshDatabase

Les Feature Tests testent des comportements complets : une requête HTTP arrive, passe par le router, le middleware, le contrôleur, Eloquent, et retourne une réponse. Ces tests requièrent une base de données — configurez SQLite en mémoire dans phpunit.xml pour que les tests soient rapides et isolés :

<!-- phpunit.xml — déjà généré par pest:install, ajouter ces env -->
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
php artisan pest:test Feature/ArticleApiTest
<?php
// tests/Feature/ArticleApiTest.php

use AppModelsArticle;
use AppModelsUser;
use IlluminateFoundationTestingRefreshDatabase;

uses(RefreshDatabase::class);

// Test de l'endpoint public — liste des articles
it('retourne la liste paginée des articles publiés', function () {
    Article::factory()->count(5)->published()->create();
    Article::factory()->count(3)->create(['status' => 'draft']); // Ne doivent pas apparaître

    $response = $this->getJson('/api/articles');

    $response->assertOk()
             ->assertJsonStructure([
                 'data' => [['id', 'title', 'slug', 'status', 'author']],
                 'meta' => ['current_page', 'total', 'per_page'],
             ])
             ->assertJsonCount(5, 'data'); // Seulement les publiés
});

// Test de création d'article — requiert authentification
it('un utilisateur authentifié peut créer un article', function () {
    $user = User::factory()->create();

    $response = $this->actingAs($user)->postJson('/api/articles', [
        'title'   => 'Mon nouvel article de test',
        'content' => str_repeat('Contenu de test. ', 10), // 50+ caractères
    ]);

    $response->assertCreated()
             ->assertJsonPath('data.title', 'Mon nouvel article de test')
             ->assertJsonPath('data.status', 'draft');

    $this->assertDatabaseHas('articles', [
        'title'   => 'Mon nouvel article de test',
        'user_id' => $user->id,
    ]);
});

// Test d'accès non autorisé
it('un utilisateur non authentifié ne peut pas créer d'article', function () {
    $this->postJson('/api/articles', [
        'title'   => 'Article non autorisé',
        'content' => str_repeat('x', 50),
    ])->assertUnauthorized(); // 401
});

// Test de validation
it('refuse la création sans titre', function () {
    $user = User::factory()->create();

    $this->actingAs($user)->postJson('/api/articles', [
        'content' => str_repeat('Contenu valide. ', 5),
    ])->assertUnprocessable() // 422
       ->assertJsonValidationErrors(['title']);
});

Le trait RefreshDatabase garantit que chaque test commence avec une base de données propre — les migrations sont exécutées une seule fois par suite (pas à chaque test), et les données créées dans un test n’affectent pas les autres. La méthode actingAs($user) simule une session authentifiée sans passer par le flux de connexion — c’est l’équivalent de Sanctum::actingAs() pour les sessions standard.

Note sur les states factory : Le state published() utilisé dans Article::factory()->published()->create() doit être défini dans ArticleFactory :

<?php
// database/factories/ArticleFactory.php

public function published(): self
{
    return $this->state(fn (array $attributes) => [
        'status'       => 'published',
        'published_at' => now(),
    ]);
}

Une fois ce state défini, vous pouvez chaîner ->published() sur n’importe quelle factory pour générer des articles publiés en masse dans vos tests.

Étape 5 — Tester les routes Sanctum avec actingAs

Pour tester des routes protégées par Sanctum avec des tokens API, utilisez Sanctum::actingAs() qui injecte un token fictif valide pour la durée du test :

<?php
// tests/Feature/Auth/AuthTest.php

use AppModelsUser;
use IlluminateFoundationTestingRefreshDatabase;
use LaravelSanctumSanctum;

uses(RefreshDatabase::class);

it('émet un token lors de la connexion', function () {
    $user = User::factory()->create();

    $this->postJson('/api/login', [
        'email'       => $user->email,
        'password'    => 'password', // Mot de passe par défaut des factories
        'device_name' => 'test-device',
    ])->assertOk()
       ->assertJsonStructure(['token']);
});

it('accède aux ressources protégées avec un token Sanctum valide', function () {
    $user = User::factory()->create();

    // Sanctum::actingAs simule un token valide avec les abilities données
    Sanctum::actingAs($user, ['articles:read']);

    $this->getJson('/api/user')->assertOk()->assertJsonPath('id', $user->id);
});

it('refuse l'accès sans token Sanctum', function () {
    $this->getJson('/api/user')->assertUnauthorized();
});

Étape 6 — Architecture Testing avec les Arch Presets Laravel

Pest 3 introduit les arch presets : des jeux de règles architecturales prédéfinies qui vérifient que votre code respecte les conventions du framework. Pour Laravel, le preset officiel valide que les controllers ne contiennent pas de logique métier directe, que les modèles étendent bien Model, que les jobs implémentent ShouldQueue, etc. Ces tests s’exécutent sur le code source (analyse statique) sans toucher la base de données :

<?php
// tests/Arch/LaravelArchTest.php

// Le preset Laravel valide les conventions architecturales de l'écosystème
arch()->preset()->laravel();

// Règles personnalisées supplémentaires
arch('les contrôleurs ne contiennent pas de logique métier directe')
    ->expect('App\Http\Controllers')
    ->not->toUseStrict()             // Pas de logique complexe
    ->not->toHavePublicMethods();    // Uniquement les méthodes resource standards

arch('les modèles étendent Eloquent Model')
    ->expect('App\Models')
    ->toExtend('Illuminate\Database\Eloquent\Model');

arch('les jobs implémentent ShouldQueue')
    ->expect('App\Jobs')
    ->toImplement('Illuminate\Contracts\Queue\ShouldQueue');

Ces tests échouent silencieusement si votre architecture dérive — par exemple si un développeur ajoute une requête SQL directe dans un contrôleur au lieu de passer par Eloquent. Les arch presets fonctionnent comme des « linters » architecturaux intégrés dans votre pipeline CI.

Étape 7 — Lancer les tests et interpréter les rapports

Pest offre plusieurs modes d’exécution utiles au quotidien :

# Lancer tous les tests
./vendor/bin/pest

# Lancer avec couverture de code (nécessite Xdebug ou PCOV)
./vendor/bin/pest --coverage --min=80

# Lancer en mode watch (relance à chaque sauvegarde de fichier)
./vendor/bin/pest --watch

# Filtrer par nom de test
./vendor/bin/pest --filter="retourne la liste"

# Lancer un seul fichier
./vendor/bin/pest tests/Feature/ArticleApiTest.php

# Mode parallèle (intégré nativement depuis Pest 2 — pas d'install séparée)
./vendor/bin/pest --parallel

Le rapport Pest affiche chaque test avec son nom descriptif en vert (✓) ou rouge (✗). En cas d’échec, la diff entre la valeur attendue et la valeur reçue est affichée directement dans le terminal — plus besoin d’ouvrir les logs. La couverture de code génère un rapport HTML dans build/coverage/ qui visualise les lignes testées et non testées par fichier.

Erreurs fréquentes

Erreur Cause Solution
Class RefreshDatabase not found Oubli du uses(RefreshDatabase::class) en tête de fichier Ajouter uses(RefreshDatabase::class); ou le configurer globalement dans tests/Pest.php
Tests lents (plusieurs secondes chacun) SQLite fichier au lieu de :memory: dans phpunit.xml Mettre DB_DATABASE=:memory: dans les env de test
actingAs ne protège pas les routes Sanctum Utilisation de actingAs() au lieu de Sanctum::actingAs() pour les routes auth:sanctum Importer use LaravelSanctumSanctum; et utiliser Sanctum::actingAs($user)
Factory inexistante pour un modèle Modèle créé sans -f flag ou sans HasFactory trait Ajouter use HasFactory; dans le modèle et créer la factory avec php artisan make:factory ArticleFactory

FAQ

Pest est-il compatible avec les tests PHPUnit existants ?
Oui. Pest s’exécute sur PHPUnit. Les classes de test PHPUnit classiques (class FooTest extends TestCase) fonctionnent sans modification dans un projet Pest. Vous pouvez migrer progressivement.

Comment configurer RefreshDatabase globalement pour tous les Feature tests ?
Dans tests/Pest.php, ajoutez : uses(RefreshDatabase::class)->in('Feature');. Tous les fichiers dans tests/Feature/ bénéficieront automatiquement du trait sans avoir à le déclarer dans chaque fichier.

Qu’est-ce que le Mutation Testing de Pest 3 ?
Le mutation testing introduit des petites modifications dans votre code source (inverser un opérateur === en !==, supprimer un return) et vérifie si vos tests détectent ces mutations. Un test qui ne détecte pas une mutation est un test insuffisant. Activez-le avec ./vendor/bin/pest --mutate (nécessite la configuration appropriée dans pest.php).

Tutoriels frères dans cette série

Pour aller plus loin

Service ITSkillsCenter

Site ou application web sur mesure

Conception Pro + Nom de domaine 1 an + Hébergement 1 an + Formation + Support 6 mois. Accès et code livrés. À partir de 350 000 FCFA.

Demander un devis
Publicité