📍 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
- Projet Laravel 11 — guide d’installation
- Au moins un modèle et un contrôleur créés — voir le tutoriel API REST
- Connaissances de base en tests (notion de assertion, mock, factory)
- Temps estimé : 45 à 60 minutes
É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
- Créer une API REST avec Laravel 11
- Eloquent ORM dans Laravel 11
- Authentification avec Laravel Sanctum
- Queues et jobs Laravel 11