Pest 5 est arrivé en juillet 2026 avec une promesse séduisante : ne relancer que les tests touchés par votre diff. Mais Pest est né dans l’écosystème Laravel, et ses chiffres spectaculaires viennent de suites Laravel. Nous l’avons donc installé sur un squelette Symfony — Symfony 8.1.9, PHP 8.5.11, Pest 5.3.2, PHPUnit 13.4.2, le tout en Docker — pour faire le tri entre ce qui se transpose, ce qui piège et ce qui ne vous concerne pas.
Pest 5 est un framework de test PHP construit sur PHPUnit 13 ; il s’utilise sans Laravel, y compris sur une application Symfony. Il exige PHP 8.4 et PHPUnit 13. Annoncé à Laracon US et présenté dans l’article officiel du 29 juillet 2026, il en est à la version 5.3.2 (12 octobre).
Pest 5 : ce qui change vraiment
Le guide de migration de Pest 4 vers 5 tient en une ligne — passer pestphp/pest et ses plugins en ^5.0 — et ne liste aucune rupture propre à Pest. La vraie friction est PHPUnit 13, que Pest embarque : le chemin 11.5, 12.5 puis 13 est détaillé dans notre article sur PHPUnit 13 sur Symfony.
- Test Impact Analysis (
--tia) : n’exécuter que les tests impactés par les fichiers modifiés. C’est la nouveauté phare de la 5. - Sharding équilibré par durée : répartir la suite sur plusieurs jobs CI de durée homogène. Arrivé en Pest 4.6.0, pas en 5.
- Tests navigateur via Playwright : arrivés en Pest 4, en août 2025.
- Plugins PHPStan, Rector, Agent et Evals.
Les chiffres mis en avant — une suite de 19 000 tests passée de trois minutes à cinq secondes, par exemple — viennent de projets Laravel. Citez-les comme tels, pas comme une promesse pour votre application.
Installer Pest 5 sur une app Symfony : trois pièges
- Le plugin Composer est bloqué. Avec Composer 2.10,
composer require --dev pestphp/pestéchoue : « pestphp/pest-plugin contains a Composer plugin which is blocked by your allow-plugins config ». C’est le comportement voulu ; autorisez-le explicitement. - Le namespace généré est faux.
pest --initcréetests/Pest.phpettests/TestCase.phpdans le namespaceTests\, alors que Symfony déclareApp\Tests\. Première exécution : « The class or trait [Tests\TestCase] could not be found ». Corrigez les deux fichiers. - La configuration Symfony est conservée — bonne nouvelle cette fois : la recette Flex de PHPUnit crée
phpunit.dist.xmlettests/bootstrap.php, que Pest utilise tels quels.
composer config --no-plugins allow-plugins.pestphp/pest-plugin true
composer require --dev "pestphp/pest:^5.0" --with-all-dependencies
vendor/bin/pest --init # puis corriger le namespace : Tests\ -> App\Tests\
vendor/bin/pest --version # Pest Testing Framework 5.3.2Sur du code de domaine — value objects, calculateurs —, Pest est un confort de syntaxe au-dessus de PHPUnit : expect(), datasets nommés, ->throws(). Les datasets nommés donnent des sorties lisibles (« it applique la TVA with dataset "TVA 5,5 %" »).
<?php
// tests/Unit/PriceCalculatorTest.php
declare(strict_types=1);
use App\Domain\Money;
use App\Domain\PriceCalculator;
it('applique la TVA', function (int $net, float $rate, int $expected): void {
$gross = (new PriceCalculator())->withVat(new Money($net), $rate);
expect($gross)->toBeInstanceOf(Money::class)
->and($gross->cents)->toBe($expected);
})->with([
'TVA 20 %' => [10000, 0.20, 12000],
'TVA 5,5 %' => [10000, 0.055, 10550],
'TVA 0 %' => [10000, 0.0, 10000],
]);
it('refuse un montant négatif', function (): void {
new Money(-1);
})->throws(InvalidArgumentException::class, 'Montant négatif');Arch tests : le faux négatif à connaître
Un arch test est une règle d’architecture exécutée comme un test. C’est la fonction de Pest la plus transposable à Symfony : elle opère sur le code PHP, pas sur le framework, et convient parfaitement à une architecture hexagonale. Mais la règle la plus intuitive est un piège. arch()->expect('App\Domain')->not->toUse('Symfony') passe toujours, même quand le domaine importe Symfony\Component\Console\Application. Nous l’avons vérifié sous trois formes : import de classe, type de paramètre, new.
La cause est dans le code du plugin : le namespace interdit est résolu à partir des préfixes PSR-4 enregistrés dans l’autoloader Composer. Symfony est plus court que tout préfixe réellement enregistré, du type Symfony\Component\Console\ : la couche visée est vide, et l’assertion est vraie par défaut. Ce qui fonctionne — vérifié en rouge puis en vert — c’est une liste blanche, ou des préfixes PSR-4 complets :
<?php
// tests/ArchTest.php (le nom doit finir par Test.php pour être découvert)
declare(strict_types=1);
// FAUX NÉGATIF : passe même si le domaine dépend de Symfony
// arch()->expect('App\Domain')->not->toUse('Symfony');
// Liste blanche : échoue avec « However, it also uses
// 'Symfony\Component\Console\Application' »
arch('le domaine ne dépend que du domaine')
->expect('App\Domain')
->toOnlyUse(['App\Domain']);
// Interdictions ciblées : préfixes PSR-4 complets uniquement
arch('pas de HTTP ni de Console dans le domaine')
->expect('App\Domain')
->not->toUse([
'Symfony\Component\HttpFoundation',
'Symfony\Component\Console',
]);
arch('domaine strict et final')
->expect('App\Domain')
->toUseStrictTypes()
->classes->toBeFinal();
arch('pas de debug oublié')->expect(['dd', 'dump', 'var_dump'])->not->toBeUsed();Si vous utilisez déjà Deptrac, la question n’est pas de remplacer mais de ne pas doublonner : Deptrac décrit des couches dans un fichier dédié, les arch tests vivent dans la suite. Notre article sur la protection de l’architecture Symfony avec Deptrac détaille l’autre approche. Dans les deux cas, écrivez d’abord un test qui doit échouer, et vérifiez qu’il échoue.
Parallélisme et sharding par durée en CI
--parallel s’appuie sur ParaTest et lance par défaut un processus par cœur. Sur une petite suite, le gain est nul : le coût de démarrage domine. Le sharding par durée se pilote en deux temps : enregistrer les durées par classe de test dans tests/.pest/shards.json, committer ce fichier, puis répartir. Sans lui, --shard=1/2 répartit par nombre de fichiers ; avec, la sortie indique « (time-balanced) », et sur notre suite le seul test lent — l’arch test, à 0,7 seconde — s’est bien retrouvé seul sur son shard.
Votre dette technique s'accumule ?
Audit complet en 10 jours. Recommandations priorisées et actionnables.
# 1) Enregistrer les durées, puis committer le fichier
vendor/bin/pest --update-shards
git add tests/.pest/shards.json
# 2) En CI, un job par shard (matrice de 4 jobs)
vendor/bin/pest --shard=1/4Les clés du fichier sont les noms complets des classes de test : régénérez-le après un gros refactoring. Visez au moins la 5.3.2, qui corrige des filtres de shard débordant de leur partition. Le sharding ne règle pas l’isolation : chaque job a besoin de sa propre base de test si vos tests fonctionnels en utilisent une.
Test Impact Analysis : ça marche, avec quatre prérequis
Verdict : le TIA fonctionne sur un projet Symfony. Mais la documentation résume ses prérequis en une ligne, et nous les avons découverts un par un :
- Au moins un commit, sinon « Tia mode requires at least one commit ».
- Un remote git, ou une branche par défaut déclarée dans
tests/Pest.php:pest()->tia()->defaultBranch('main');. - Un driver de couverture, pcov ou Xdebug : c’est la couverture qui construit le graphe entre tests et fichiers.
- Un répertoire HOME persistant : le graphe est stocké sous
$HOME/.pest/tia/. Dans undocker run --rm, HOME disparaît à chaque exécution : tout est réenregistré et rejoué, sans aucun message d’erreur. Montez un volume sur HOME. C’est le piège qui fera dire à une équipe sous Docker que « le TIA ne sert à rien ».
Les mesures sur notre petite suite de neuf tests : premier passage pour enregistrer, 1,05 seconde ; second passage sans changement, « 9 replayed » en 0,07 seconde. Après modification de PriceCalculator.php, seuls les deux fichiers de test concernés sont relancés. Et un test cassé par un changement dans Money.php est bien détecté en mode --parallel --tia. Les modifications de commentaires PHP, elles, ne déclenchent rien.
Les angles morts sont ceux d’un graphe construit sur la couverture PHP. Une modification de config/services.yaml a relancé toute la suite : Pest traite prudemment un fichier qu’il ne sait pas relier — c’est sûr, mais sans gain. Une ligne ajoutée à .env.test a été ignorée ; aucun de nos tests ne lisait ce fichier, donc nous ne pouvons pas conclure. Si vos tests dépendent de Twig, de migrations Doctrine ou de variables d’environnement, comparez la sélection du TIA à la suite complète sur une dizaine de diffs réels avant de lui faire confiance. La sortie affiche d’ailleurs « Experimental TIA mode enabled », et la documentation le réserve à l’usage local : en CI, gardez la suite complète.
Tests navigateur : utilisables, mais pas clés en main
Le plugin pestphp/pest-plugin-browser n’exige pas Laravel : il dépend d’amphp et de symfony/process. Ses prérequis réels sont l’extension sockets — sans elle, Composer refuse l’installation — ainsi que Node et Playwright. La limite Laravel-centrée apparaît vite : visit('/hello') échoue sous Symfony (« Cannot navigate to invalid URL »), car la résolution des URL relatives suppose l’application Laravel démarrée dans le processus de test. Sous Symfony, démarrez votre serveur vous-même et passez une URL absolue :
<?php
// tests/Browser/HelloTest.php : serveur Symfony déjà lancé sur :8000
declare(strict_types=1);
it('affiche la page dans un vrai navigateur', function (): void {
visit('http://127.0.0.1:8000/hello') // URL absolue obligatoire hors Laravel
->assertSee('Bonjour Symfony')
->assertNoJavaScriptErrors()
->assertTitle('ok');
});Ce test passe. Mais l’état — base de test, fixtures — doit être préparé côté serveur, sans transaction partagée avec le processus de test. Panther, qui démarre lui-même le serveur depuis un WebTestCase, reste mieux intégré au kernel ; le plugin Pest apporte la syntaxe fluide et l’option --trace.
Les plugins : PHPStan oui, le reste selon vos besoins
- PHPStan (
pest-plugin-phpstan) est utile et indépendant du framework : il détecte les expectations impossibles, comme « Calling toBeString() on Expectation<App\Domain\Money>; assertion is impossible ». Faux positif rencontré : un helper deTestCaseappelé viatest()->bootKernel()est signalé introuvable alors que le test passe. - Rector (
pest-plugin-rector) convertit des assertions PHPUnit en expectations Pest. Utile pour migrer une suite, vérifiable en--dry-run; les chaînes->and()produites ne sont pas toujours lisibles. - Agent exécute un extrait de code dans un vrai test éphémère, pour la boucle des agents de code. Il fonctionne sur un domaine Symfony, sans remplacer des tests commités.
- Evals note des sorties de modèles de langage : pertinent seulement si votre application embarque de l’IA générative. Nous ne l’avons pas testé.
Verdict : quoi adopter sur Symfony
- Si vous êtes déjà en PHP 8.4 et PHPUnit 13, Pest 5 coûte peu : PHPUnit et ParaTest restent en dessous, votre configuration Symfony est conservée.
- Si vous êtes encore en PHPUnit 11 ou 12, migrez PHPUnit d’abord, pour ses propres raisons, puis jugez Pest sur pièces.
- À adopter : le sharding par durée, les arch tests écrits avec
toOnlyUse, le plugin PHPStan, et le TIA en local avec un HOME persistant. - Avec prudence : les tests navigateur, qui demandent d’orchestrer serveur et données, et le TIA sur des suites qui dépendent de YAML, de Twig ou de l’environnement.
- À ignorer sauf besoin précis : Evals, et les chiffres de performance issus de suites Laravel.
En CI, gardez la suite complète répartie en shards ; en local, --parallel --tia ; et ajoutez une règle toOnlyUse avant de vous croire protégé par vos arch tests. Une suite rapide ne vaut que par ce qu’elle vérifie : le mutation testing avec Pest sur Symfony reste le meilleur moyen de le mesurer, et le plugin de mutation est installé d’office avec Pest 5.
Accélérer une suite de tests n’a de sens que si l’on sait ce qu’elle protège : un TIA ou un sharding bien réglés ne compensent ni une couverture creuse ni des couches qui se mélangent. Si vous voulez un état des lieux factuel de vos tests, de votre CI et de votre dette, c’est ce que couvre le Bear Scan : un audit en dix jours, avec des recommandations priorisées que votre équipe peut appliquer seule.

Écrit par
Flavien Métivier
Expert PHP/Symfony indépendant depuis 2014, ancien CTO de Kartable et d'Extrabat. Plus de 80 projets menés, de l'audit à la migration.
Mon parcours