Aller au contenu principal

Écrire des tests unitaires PHP avec Pest

Maîtrisez les tests unitaires PHP avec Pest ! Découvrez l'installation facile, la syntaxe expressive expect, les datasets, les snapshots et les puissantes fonctionnalités de tests architecturaux.

· 10 minutes de lecture
Voir la source
Christophe
Amoureux de Markdown, WSL et Docker ~ Curieux insatiable.
Cet article existe aussi en anglais — lire la version originale.

Traduction automatique

Cette traduction a été produite par une IA. Elle peut contenir des contresens ou des maladresses, en particulier sur les termes techniques. En cas de doute, consultez l'article de référence en anglais — c'est lui qui fait foi.

Journal des modifications

Dernière
Pest v5 released at Laracon US 2026 (requires PHP 8.4 + PHPUnit 13); core unit-testing API unchanged.

Écrire des tests unitaires PHP avec Pest

⚡
TL;DR

Voici une référence complète sur Pest, le wrapper expressif de PHPUnit pour PHP : installation, la syntaxe it()/test()/expect(), assertions vs expectations, datasets inline et partagés, réutilisation de cas de test PHPUnit existants, tests architecturaux (imposer des règles comme « pas de validation dans les controllers »), snapshot testing, fonctions globales personnalisées dans tests/Pest.php, conversion depuis PHPUnit, et outillage VSCode (Better Pest, Pest Snippets) y compris la configuration avec un container Docker.

Si vous pensez qu'écrire des tests unitaires en PHP est amusant, restez dans la salle ; les autres, sortez s'il vous plaît. Et là tout le monde sort, y compris celui qui a posé la question.

Écrire des tests unitaires à l'ancienne avec PHPUnit est tellement ennuyeux que presque personne ne le fait.

Et depuis un certain temps, https://pestphp.com/ est arrivé et a totalement changé la façon de faire.

Pest est un wrapper autour de PhpUnit donc, par exemple, chaque argument en ligne de commande supporté par PhpUnit peut être utilisé avec Pest.

Les tests unitaires ne sont qu'une couche d'une stratégie qualité ; les autres, sur ce blog, sont l'analyse statique avec jakzal/phpqa et les hooks pre-commit, qui tournent tous les deux avant qu'un commit puisse passer.

Vous cherchez plutôt des tests navigateur/fonctionnels ?

Cet article se concentre sur les tests unitaires classiques. Pest v4 a ajouté le testing navigateur intégré ; Pest v5 (actuel, juillet 2026) ajoute le Test Impact Analysis et requiert PHP 8.4 — voir Écrire des tests fonctionnels avec Pest. Vous testez des scripts Bash ? Jetez un œil à Lancer des tests unitaires avec bats/bats.

Résultat​

Un fichier de test, et la ligne de bootstrap qui le relie au TestCase de PHPUnit :

uses(Tests\TestCase::class)->in('Features');

Ce fichier configure Pest pour exécuter tous les tests situés dans le répertoire Features en utilisant la classe TestCase du projet. C'est une configuration globale qui établit le contexte de base pour une suite de tests.

<?php
test('assert true is true', function () {
expect(true)->toBeTrue();
});
test('assert false is not true', function () {
expect(false)->not->toBeTrue(); // we can also write `not()->`
});

Ce fichier définit deux tests unitaires simples utilisant un framework de test (probablement Pest). Le premier test vérifie qu'une valeur vraie est bien vraie, et le second teste que une valeur fausse n'est pas vraie en utilisant l'opérateur de négation.

Le lancer avec ./vendor/bin/pest tests/Feature/MyFirstTest.php donne :

>user@machine: ~/yourproject

Pourquoi Pest​

  • Les tests se lisent comme des phrases naturelles : expect($value)->toBeTrue() au lieu du $this->assertTrue($value) de PHPUnit — les méthodes peuvent être niées avec not->.
  • it() et test() sont interchangeables ; utilisez celui qui correspond à votre convention de nommage, les deux partagent le même comportement et la même syntaxe.
  • Pest est un wrapper autour de PHPUnit, pas un remplaçant : chaque argument en ligne de commande de PHPUnit fonctionne toujours, et les cas de test PHPUnit existants peuvent être réutilisés tels quels (il suffit d'ajouter /** @test */).

Installation​

https://pestphp.com/docs/installation#installation

Lancez simplement les commandes ci-dessous dans le dossier de votre application PHP :

>user@machine: ~/yourproject

À partir de maintenant, on peut lancer ./vendor/bin/pest pour exécuter nos tests Pest.

Écrire des tests​

Introduction à Pest​

Les fichiers doivent avoir le suffixe Test​

Comme PHPUnit, Pest traitera tous les fichiers des dossiers tests/Feature et tests/Unit ayant le suffixe Test comme par exemple ShoppingBasketTest.php.

Que signifie $this dans un test ?​

Dans notre fichier tests/Pest.php, on a la ligne de bootstrap montrée en haut de cet article.

Dans un test Pest, $this fait référence à la classe PHPUnit Tests\TestCase.

it ou test​

Pest nous laisse le choix entre it() et test(). Utilisez celui qui correspond le mieux à votre convention de nommage des tests, ou les deux. Ils partagent le même comportement et la même syntaxe.

Pour en savoir plus : https://pestphp.com/docs/writing-tests#api-reference

Le résultat est le même, seule la sortie dans la console diffère.

Nos premiers tests​

C'est le test MyFirstTest.php et sa sortie PASS montrés en haut de cet article. Il illustre que Pest démarre avec un verbe expect et une méthode comme toBeTrue(). Les méthodes peuvent être niées avec not-> (https://pestphp.com/docs/expectations#expect-not).

Autocomplétion​

Veillez à installer et activer PHP Intelephense et profitez de l'autocomplétion de vscode.

Différence entre toBe et toEqual​

<?php
test('assert count is correct', function () {
expect(2 + 2)->toBe(4); // Will be true
expect(2 + 2)->toBe('4'); // Will NOT be true
expect(2 + 2)->toEqual(4); // Will be true
expect(2 + 2)->toEqual('4'); // Will be true
});

Ce test démontre la différence entre deux méthodes de comparaison : toBe() fait une comparaison stricte (type et valeur doivent correspondre), tandis que toEqual() fait une comparaison souple où 4 et '4' sont considérés comme égaux.

toBe sera plus strict, c'est-à-dire qu'il vérifiera à la fois la valeur et le type de donnée, alors que toEqual vérifiera uniquement la valeur.

Assertions​

https://pestphp.com/docs/assertions

Les assertions viennent de PhpUnit et fonctionnent de la même manière.

Les assertions sont accessibles via l'objet $this, et ce parce que tests/pest.php contient la ligne ci-dessous.

uses(Tests\TestCase::class)->in('Feature');

Ce fichier configure PestPHP pour exécuter tous les tests situés dans le dossier Feature en les traitant comme des instances de la classe TestCase. Cela établit l'environnement de base et les fonctionnalités communes que tous ces tests vont utiliser.

Donc $this fait référence à la classe PHPUnit Tests\TestCase.

Expectations​

https://pestphp.com/docs/expectations

En plus des assertions, Pest vous propose un ensemble d'expectations. Ces fonctions permettent de tester vos valeurs contre certaines conditions. Cette API est inspirée de Jest. Les expectations vous permettent aussi d'écrire vos tests comme une phrase naturelle.

Assertions et expectations peuvent être utilisées dans les fichiers de test Pest mais... les expectations sont plus explicites et intuitives.

<?php
test('assert true is true', function () {
// These two lines do exactly the same. Keep just one...
$this->assertTrue(true);
expect(true)->toBeTrue();
});

Ce test vérifie que la valeur true est bien true en utilisant deux syntaxes différentes de test — l'ancienne méthode avec assertTrue et la nouvelle avec expect. Le commentaire indique qu'une seule de ces deux assertions est nécessaire et qu'il faudrait supprimer l'autre pour éviter la redondance.

Utiliser les datasets​

https://pestphp.com/docs/datasets

On a plusieurs façons de fournir des données à une fonction.

Voici la version inline

it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'enunomaduro@gmail.com',
'other@example.com'
]);

Ce test vérifie que chaque email fourni dans le tableau n'est pas vide. Pest exécute le test plusieurs fois, une fois par email, pour s'assurer que tous les emails passent cette validation.

Le dataset est alors un tableau et on peut avoir un tableau multidimensionnel :

it('has emails', function (string $name, string $email) {
expect($email)->not->toBeEmpty();
})->with([
['John', 'enunomaduro@gmail.com'],
['Other', 'other@example.com']
]);

Ce test paramétrisé vérifie que chaque adresse email fournie n'est pas vide. La méthode with() injecte plusieurs ensembles de données (nom et email) dans le test, ce qui exécute le test une fois pour chaque paire de valeurs.

Il existe aussi un moyen de créer un dataset partagé, ce qui est probablement mieux quand le fichier de test devient volumineux (https://pestphp.com/docs/datasets#shared-datasets).

Réutiliser des cas de test PHPUnit sans modification​

C'est vraiment simple : il suffit d'ajouter /** @test */ comme doc block avant le scénario de test.

Par exemple

<?php
declare(strict_types=1);
namespace Tests\Feature;
use Tests\TestCase;
class VisitLoginPageTest extends TestWebCase
{
/** @test */
public function test_we_should_see_fields_email_and_password()
{
$this->response->assertSee('id="password"', false);
$this->response->assertSee('id="email"', false);
}
}

Ce fichier de test vérifie que la page de connexion affiche correctement les champs email et password. Il utilise le framework de test de Laravel pour s'assurer que les éléments HTML avec les identifiants attendus sont présents dans la réponse HTTP.

Et à partir de maintenant ce test peut être lancé avec ./vendor/bin/pest.

Tests architecturaux​

https://pestphp.com/docs/arch-testing

Avec Pest (depuis la v2), on peut garantir certaines cohérences architecturales, comme ne pas utiliser de validations dans un controller (via $request->validate(...)) mais forcer l'usage de classes Form request à la place.

Le plugin architectural n'aide pas à lancer des tests unitaires mais scanne le projet et s'assure que certaines règles sont respectées.

Les tests architecturaux peuvent ressembler à ceci :

test('controllers')
->expect('App\Http\Controllers')
->not->toUse('Illuminate\Http\Request');
// Models can only be used in a repository
test('models')
->expect('App\Models')
->toOnlyBeUsedOn('App\Repositories')
->toOnlyUse('Illuminate\Database');
test('repositories')
->expect('App\Repositories')
->toOnlyBeUsedOn('App\Controllers')
->toOnlyUse('App\Models');
test('globals')
->expect(['dd','dump','var_dump'])
->not->toBeUsed();
test('facades')
->expect('Illuminate\Support\Facades')
->not->toBeUsed()
->ignoring('App\Providers');

Ce fichier définit des règles d'architecture pour un projet Laravel en utilisant des tests de dépendances. Il vérifie que les contrôleurs n'importent pas directement Request, que les modèles ne sont utilisés que dans les repositories, que les repositories ne sont appelés que par les contrôleurs, et que certaines fonctions globales et facades ne sont pas utilisées dans le code.

Cette partie est visible en vidéo https://youtu.be/9EGPo_enEc8?t=1021

On peut aussi vérifier si une classe est final :

test('controllers')
->expect('App\Http\Controllers')
->toUseStrictTypes()
->toHaveSuffix('Controller') // or toHavePrefix, ...
->toBeReadonly()
->toBeClasses() // or toBeInterfaces, toBeTraits, ...
->classes->toBeFinal() // 🌶
->classes->toExtendNothing() // or toExtend(Controller::class),
->classes->toImplementNothing() // or toImplement(ShouldQueue::class),

Ce test définit un ensemble de règles architecturales pour les contrôleurs de l'application. Il vérifie que tous les fichiers dans le répertoire App\Http\Controllers respectent des contraintes strictes : types déclarés, noms suffixés par Controller, propriétés en lecture seule, et classes finales qui n'héritent ni n'implémentent rien.

Prendre des snapshots​

Il y a aussi une fonctionnalité appelée Snapshots. L'idée est de stocker un contenu comme snapshot puis de comparer les exécutions futures avec ce snapshot.

Un snapshot peut être le contenu d'une page HTML, une réponse JSON, le contenu d'un fichier / d'un tableau... en fait n'importe quoi (pour un objet, on peut le sérialiser et donc le stocker aussi comme snapshot).

it('has a welcome page', function() {
$response = $this->get('/');
expect($response)->toMatchSnapshot();
});

Ce test vérifie qu'une requête GET sur la racine du site retourne une réponse valide. Il compare cette réponse à un snapshot enregistré précédemment pour s'assurer que le contenu de la page d'accueil ne change pas de façon inattendue.

Au tout premier lancement (vendor/bin/pest), le snapshot n'existe pas encore donc il sera créé sur disque et le test sera noté WARN.

Le snapshot sera créé dans un sous-dossier du dossier ./tests/.pest/snapshots (le sous-dossier correspondra à l'emplacement de votre test lancé (p.ex. Feature/ExampleTest/it_has_a_welcome_page.snap)).

À partir du deuxième lancement, le snapshot pris sera comparé avec, dans l'exemple ci-dessus, le contenu HTML de la page d'accueil. Dès qu'une différence est détectée (comme la date du jour si elle est présente sur la page), Pest l'affichera dans un diff : la chaîne précédente issue du snapshot et la chaîne récupérée, actuelle.

Écrire des fonctions globales​

On peut écrire nos propres fonctions personnalisées dans le fichier tests/Pest.php.

Le bootstrap de Pest​

Le fichier tests/pest.php peut servir à y placer des fonctions globales mais on devra aussi le mettre à jour si, dans nos fichiers de test, on a besoin d'autres classes.

uses(Tests\TestCase::class)->in('Feature');

Cette ligne configure l'environnement de test en spécifiant que tous les tests du répertoire 'Feature' doivent utiliser la classe TestCase comme classe de base. Cela permet à Pest (le framework de test) de savoir quel contexte appliquer à ces tests.

La ligne ci-dessus rendra Tests\TestCase disponible dans tous les tests de tests/Feature. Si on a besoin de plus de classes, on peut les ajouter :

uses(Tests\TestCase::class,Illuminate\Foundation\Testing\RefreshDatabase::class)->in('Feature');

Ce fichier configure Pest pour que tous les tests Feature utilisent deux traits : TestCase (la classe de base pour les tests Laravel) et RefreshDatabase (qui réinitialise la base de données après chaque test). C'est une façon centralisée de définir le comportement commun à tous les tests du dossier Feature.

Et aussi dans le dossier tests/Unit :

uses(Tests\TestCase::class)->in('Unit');

Ce fichier configure Pest pour exécuter les tests unitaires en utilisant la classe TestCase du projet. Il indique au framework de test quels fichiers charger et dans quel contexte les exécuter.

Trucs et astuces​

Optionnel.

Dump and die​

On peut utiliser la méthode dd pour afficher la valeur de l'expectation courante et arrêter la suite de tests, comme ceci :

expect($response)
->dd()
->toHaveKey('data')
->data->toBeEmpty();

Ce code teste une réponse en enchaînant plusieurs assertions pour vérifier d'abord que la clé 'data' existe, puis qu'elle est vide. Il utilise une bibliothèque de test fluide qui permet d'écrire les vérifications de manière lisible et enchaînée.

Convertir depuis PHPUnit​

Optionnel — pertinent uniquement si vous avez une suite PHPUnit existante à migrer.

Migrate from PHPUnit to Pest - Online migration tool

Introduit dans Pest depuis la v2.9, il existe un outil pour convertir de PHPUnit vers Pest.

Voir https://pestphp.com/docs/pest-spicy-summer-release#content-drift-plugin

Note : Rector propose aussi un outil : https://github.com/rectorphp/rector-pest

Par exemple, le code ci-dessous

<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
public function test_that_true_is_true(): void
{
$this->assertTrue(true);
}
}

Ce fichier définit une classe de test unitaire appelée ExampleTest qui hérite de TestCase de PHPUnit. Elle contient une seule méthode de test qui vérifie qu'une valeur booléenne true est effectivement true, ce qui est un test trivial servant généralement de vérification que le framework de test est correctement configuré.

sera converti en

test('true is true', function () {
expect(true)->toBeTrue();
});

Ce test vérifie que la valeur booléenne true est effectivement true en utilisant le framework de test Pest. C'est un test trivial et basique qui s'assure que l'assertion toBeTrue() fonctionne correctement.

Outils​

Plugin Laravel​

Laravel Pest plugin

Installez le plugin comme ceci : composer require pestphp/pest-plugin-laravel --dev

De nouvelles commandes artisan seront alors disponibles :

>user@machine: ~/yourproject

Extensions Visual Studio Code​

  • S'il n'est pas déjà installé, PHP Intelephense vous permettra d'appuyer sur F12 sur un nom de méthode (comme toBeTrue) et de sauter à l'endroit où la méthode est implémentée,
  • Better Pest et
  • Pest Snippets

Better Pest avec Docker​

Si vous utilisez Docker, pensez à ajouter les lignes suivantes dans votre fichier de configuration .vscode/settings.json :

{
"better-pest.docker.enable": true,
"better-pest.docker.command": "docker compose exec -u root:root app",
"better-pest.docker.paths": {
"/your/local/path": "/your/remote/path"
}
}

Ce fichier configure l'extension better-pest pour exécuter les tests Pest à l'intérieur d'un conteneur Docker via docker compose. Il active le support Docker, définit la commande pour accéder au conteneur avec les droits root, et mappe les chemins locaux vers les chemins distants du conteneur.

Pensez à adapter le nom de votre container (app ici) et les chemins :

  • /your/local/path est l'endroit où votre repository est stocké, sur votre machine hôte,
  • /your/remote/path est le path dans votre container, probablement /var/www/html.

Maintenant, ouvrez simplement n'importe quel fichier Pest et appuyez sur CTRL+Shift+P pour ouvrir la Command Palette. Commencez à taper Better Pest et sélectionnez l'option souhaitée (comme Better Pest: run pour lancer le fichier).

Convertir de PHPUnit vers Pest​

Le repository https://github.com/mandisma/pest-converter propose un PHPUnit to Pest Converter : PestConverter est une librairie PHP pour convertir des tests PHPUnit en tests Pest.

Conclusion​

Pest ne remplace pas PHPUnit, il remplace juste le cérémonial — expect()->toBeTrue() au lieu d'assertions verbeuses, les datasets et les tests architecturaux offerts, et chaque cas de test PHPUnit existant fonctionne toujours sans modification. Installez-le une fois, écrivez le prochain test dans la syntaxe expressive, et migrez le reste seulement quand ça vous arrange.

Vidéos​


Cet article vous a-t-il été utile ?
Avez-vous suivi les étapes de cet article ?