Écrire des tests fonctionnels avec Pest
Découvrez comment écrire des tests fonctionnels clairs et expressifs avec le framework de test PHP Pest.
Traduction automatique
Cet article fait partie de la série Tests fonctionnels :
Journal des modifications

Cet article est un guide complet pour écrire des tests fonctionnels pour votre site web avec Pest v4. Il vous accompagne dans la mise en place d'un environnement de test complet basé sur Docker, en partant de zéro : création d'un Dockerfile et d'un Makefile pour lancer les commandes facilement. Vous allez apprendre à écrire un test navigateur, à l'exécuter contre un site en ligne et à interpréter les résultats. L'article explique aussi comment Pest capture automatiquement des screenshots en cas d'échec, ce qui simplifie le débogage, et donne quelques éclairages sur la technologie sous-jacente comme Playwright.
L'année dernière, j'ai écrit Écrire des tests unitaires PHP avec Pest. Depuis, Nuno Maduro a publié Pest en version 4 avec le browser testing intégré.
Qu'est-ce que ça veut dire ? Maintenant, en plus des tests unitaires (voir mon article précédent), on peut aussi demander à Pest de visiter un site web et d'y faire des actions : cliquer sur un bouton, remplir des champs (pensez à une page de login, un champ de recherche, ...), soumettre des formulaires, naviguer, ... et donc, oui, lancer des tests fonctionnels.
Un exemple : visiter la page d'accueil de votre site, cliquer dans la zone de recherche, saisir quelques mots-clés, sélectionner le premier élément visible, cliquer dessus et vérifier que la nouvelle page affiche bien un certain texte.
J'ai déjà écrit un article sur Behat, un autre sur Cypress ; c'était logique de faire la même chose avec Pest v4.
Mon idée, comme toujours, c'est d'utiliser (ou de créer) une image Docker pour pouvoir démarrer immédiatement, sans la douleur de tout installer et configurer.
Voici le résultat : 10 tests fonctionnels exécutés dans un vrai navigateur, en moins de 26 secondes.

Et quand un test échoue — ici, volontairement — Pest prend automatiquement un screenshot de ce que le navigateur a réellement vu :

Pas de screenshot manuel, pas d'étape de débogage séparée : l'échec et la preuve arrivent ensemble. Construisons tout ça.
Créons un projet temporaire
D'abord, créons un dossier temporaire et allons-y :
Pour l'instant, il n'existe pas d'image Docker prête à l'emploi pour Pest v4, alors créons la nôtre.
Créez un fichier appelé Dockerfile dans votre dossier temporaire avec ce contenu :
# cspell:disable
# syntax=docker/dockerfile:1.4
ARG PHP_CLI_VERSION=8.3-cliARG PHP_FPM_VERSION=8.3.24-fpmARG COMPOSER_VERSION=2.8.11
ARG OS_USERID=1000ARG OS_GROUPID=1000ARG OS_USERNAME="docker"
ARG TIMEZONE="Europe/Brussels"
FROM php:${PHP_CLI_VERSION} AS builder
ARG COMPOSER_VERSION
WORKDIR /var/www/html
# Set cache environment variables for all subsequent commandsENV NPM_CONFIG_CACHE=/var/www/html/.npm-cache \ PLAYWRIGHT_BROWSERS_PATH=/var/www/html/.cache/ms-playwright
# Install system dependencies, PHP extensions, and Node.js with browser dependenciesRUN --mount=type=cache,target=/var/cache/apt,id=apt-cache,sharing=private \ set -eux && \ apt-get update && apt-get install -y --no-install-recommends \ unzip zip git curl libzip-dev gnupg2 wget ca-certificates \ libgtk-3-0 libxshmfence-dev libxkbcommon-x11-dev \ libnspr4 libnss3 libgbm1 libasound2t64 \ && docker-php-ext-install zip sockets \ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ && apt-get clean && rm -rf /var/lib/apt/lists/*
# Install ComposerRUN curl -sS https://getcomposer.org/installer \ | php -- --version=${COMPOSER_VERSION} --install-dir=/usr/local/bin --filename=composer
# Create a temporary package.json to install node dependenciesRUN echo '{ "name": "pest-browser-tests", "version": "1.0.0", "devDependencies": { "playwright": "^1.55.0", "playwright-core": "^1.55.0" } }' > package.json
# Install Node dependencies with cacheRUN npm install
# Setup Composer dependencies with cacheRUN --mount=type=cache,target=/root/.composer \ composer init --name="function_test/pestphp" --require-dev="pestphp/pest:^4.0" --no-interaction && \ composer config --no-plugins allow-plugins.pestphp/pest-plugin true && \ composer require pestphp/pest-plugin-browser --dev && \ composer install --no-interaction --prefer-dist --no-progress
FROM php:${PHP_FPM_VERSION} AS php
ARG TIMEZONEENV TZ=${TIMEZONE}
WORKDIR /var/www/html
# Install PHP and Node.js dependenciesRUN --mount=type=cache,target=/var/cache/apt,id=apt-cache,sharing=private \ set -eux && \ apt-get update && apt-get install -y --no-install-recommends \ unzip zip libzip-dev curl gnupg2 wget ca-certificates \ libgtk-3-0 libxshmfence-dev libxkbcommon-x11-dev \ libnspr4 libnss3 libgbm1 libasound2t64 \ && docker-php-ext-install pcntl zip sockets \ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ && apt-get clean && rm -rf /var/lib/apt/lists/*
ARG OS_USERIDARG OS_GROUPIDARG OS_USERNAME
RUN set -e -x \ && groupadd --gid ${OS_GROUPID} "${OS_USERNAME}" \ && useradd --password '' -G www-data,root --uid ${OS_USERID} --gid ${OS_GROUPID} -l -d "/home/${OS_USERNAME}" "${OS_USERNAME}" \ && mkdir -p "/home/${OS_USERNAME}" \ && chown -R "${OS_USERNAME}":"${OS_USERNAME}" "/home/${OS_USERNAME}"
# Copy all files from the builder stage and set permissionsCOPY --from=builder --chown=${OS_USERNAME}:${OS_USERNAME} /var/www/html /var/www/html
# Set cache and path environment variables for all subsequent commandsENV NPM_CONFIG_CACHE=/var/www/html/.npm-cache \ PLAYWRIGHT_BROWSERS_PATH="/var/www/html/.cache/ms-playwright" \ PATH="/var/www/html/node_modules/.bin:$PATH"
# Switch to our userUSER ${OS_USERNAME}
# Install Playwright browsers in the new, accessible locationRUN npx playwright install chromium
# Dev entrypoint (keep container alive)ENTRYPOINT ["tail", "-f", "/dev/null"]Ce Dockerfile construit une image Docker multi-étapes pour exécuter des tests de navigateur Playwright avec PHP et Composer. La première étape (builder) installe PHP CLI, Node.js, Composer et les dépendances Playwright, tandis que la seconde étape (php) crée une image FPM PHP légère qui copie les artefacts du builder, configure un utilisateur non-root, et prépare l'environnement pour exécuter les tests.
Pour nous faciliter la vie, créons aussi un makefile afin de pouvoir lancer des commandes très simples comme make build, make up, ...
Créez un fichier appelé makefile dans votre projet avec ce contenu :
# Default value. To override it, run something like "WEBSITE=my_website make start"WEBSITE ?= https://www.avonture.be
UID:=$(shell id -u)GID:=$(shell id -g)
.PHONY: buildbuild: clear docker build --build-arg OS_USERID=${UID} --build-arg OS_GROUPID=${GID} --tag pestphp .
.PHONY: removeremove: docker rm demo_pest --force
.PHONY: upup: docker run --detach -v ./tests:/var/www/html/tests --name demo_pest pestphp
.PHONY: bashbash: docker exec -it demo_pest /bin/bash
.PHONY: startstart: docker exec -it demo_pest sh -c "WEBSITE=$(WEBSITE) vendor/bin/pest"Ce Makefile configure et gère un conteneur Docker pour exécuter des tests Pest. Il permet de construire une image Docker avec les permissions utilisateur appropriées, de démarrer/arrêter le conteneur, et de lancer les tests contre un site web spécifié (par défaut https://www.avonture.be).
Et ... on y est presque. Il ne reste plus qu'à créer un script de test Pest. Créez un fichier appelé tests/Browser/HomepageTest.php avec ce contenu :
<?php
it('loads the homepage', function () { visit(getenv('WEBSITE')) ->assertSee('Avonture');});
it('displays my name somewhere', function () { visit(getenv('WEBSITE')) ->assertSee('Christophe Avonture');});
it('shows blog posts on the blog page', function () { visit(getenv('WEBSITE') . '/blog') ->assertSee('All posts');});
it('navigates to the blog from the homepage', function () { visit(getenv('WEBSITE')) ->click('Blog') ->assertPathIs('/blog') ->assertSee('All Posts');});
it('loads a specific blog post', function () { visit(getenv('WEBSITE') . '/blog/docker-joomla') ->assertSee('Create your Joomla website using Docker');});
it('displays the about page', function () { visit(getenv('WEBSITE') . '/about') ->assertSee('My name is Christophe Avonture');});
it('can search for a post', function () { visit(getenv('WEBSITE')) ->click('.DocSearch-Button') ->type('.DocSearch-Input', 'Bluesky') ->assertSee('Create our own Docusaurus React component');});
it('navigates to the archive and click on aeSecure QuickScan article', function () { visit(getenv('WEBSITE') . '/blog/archive') ->click('August 1 - aeSecure - QuickScan - Free viruses scanner') ->assertPathIs('/blog/aesecure-quickscan') ->assertSee('Just start your browser and go to the URL where your site is accessible.');});
it('shows 404 for non-existent page', function () { visit(getenv('WEBSITE') . '/this-page-does-not-exist') ->assertSee('Page Not Found');});
it('can load the robots.txt file', function () { visit(getenv('WEBSITE') . '/robots.txt') ->assertSee('User-agent');});Ce fichier contient une série de tests automatisés qui vérifient le fonctionnement d'un site web personnel. Chaque test utilise la fonction it() pour visiter différentes pages du site via getenv('WEBSITE') et confirmer que le contenu attendu s'affiche correctement, en testant des éléments comme la page d'accueil, le blog, la navigation et la gestion des erreurs.
Notre situation actuelle
- Nous allons créer notre propre image Docker grâce à notre
Dockerfile, - Nous allons utiliser un makefile pour nous simplifier la vie et
- Nous avons un scénario de test avec 10 tests fonctionnels.
Créer l'image
Pour créer l'image Docker, lancez make build :
Vous n'avez pas encore make ? (cliquez pour les détails)
Lancez sudo apt-get update && sudo apt-get -y install make.
La phase de build peut être lente parce qu'il y a beaucoup de choses à télécharger. L'image finale pèsera environ 2 GB.

Créer le container
Une fois l'image créée, on peut maintenant créer notre container Docker :
Lancer les tests
Et, dernière chose à faire, une fois le container créé, vous pouvez lancer tous les tests simplement avec make start.
J'ai volontairement introduit une erreur pour illustrer une fonctionnalité sympa :
Comme on peut le voir, le test it can search for a post a échoué alors que les précédents ont réussi.
Remarque
Vous avez remarqué que les autres tests ont aussi été lancés ? Pest ne s'arrête pas au premier échec.
Le truc sympa : Pest a automatiquement pris une capture. Regardez le message d'erreur, il indique qu'un fichier Tests/Browser/Screenshots/it_can_search_for_a_post a été créé automatiquement.

Pest a pris un screenshot — le même que celui déjà montré en haut de cet article ; nous recherchions Create our own Docusaurus React component (comme codé dans notre scénario tests/Browser/HomepageTest.php) et en effet, on ne voit pas du tout ce titre. Mais on voit React component and provide a "Share on Bluesky" button, alors mettons à jour le script :
<?php
it('loads the homepage', function () { visit(getenv('WEBSITE')) ->assertSee('Avonture');});
it('displays my name somewhere', function () { visit(getenv('WEBSITE')) ->assertSee('Christophe Avonture');});
it('shows blog posts on the blog page', function () { visit(getenv('WEBSITE') . '/blog') ->assertSee('All posts');});
it('navigates to the blog from the homepage', function () { visit(getenv('WEBSITE')) ->click('Blog') ->assertPathIs('/blog') ->assertSee('All Posts');});
it('loads a specific blog post', function () { visit(getenv('WEBSITE') . '/blog/docker-joomla') ->assertSee('Create your Joomla website using Docker');});
it('displays the about page', function () { visit(getenv('WEBSITE') . '/about') ->assertSee('My name is Christophe Avonture');});
it('can search for a post', function () { visit(getenv('WEBSITE')) ->click('.DocSearch-Button') ->type('.DocSearch-Input', 'Bluesky') // highlight-next-line ->assertSee('React component and provide a "Share on Bluesky" button');});
it('navigates to the archive and click on aeSecure QuickScan article', function () { visit(getenv('WEBSITE') . '/blog/archive') ->click('August 1 - aeSecure - QuickScan - Free viruses scanner') ->assertPathIs('/blog/aesecure-quickscan') ->assertSee('Just start your browser and go to the URL where your site is accessible.');});
it('shows 404 for non-existent page', function () { visit(getenv('WEBSITE') . '/this-page-does-not-exist') ->assertSee('Page Not Found');});
it('can load the robots.txt file', function () { visit(getenv('WEBSITE') . '/robots.txt') ->assertSee('User-agent');});Ce fichier contient une suite de tests de navigation pour vérifier le fonctionnement d'un site web personnel. Chaque test visite une URL spécifique du site et vérifie que le contenu attendu s'affiche correctement — on teste l'accueil, les pages de blog, la recherche, et d'autres fonctionnalités essentielles.
Pas besoin de reconstruire l'image ni de recréer le container ; il suffit de relancer make start.
Félicitations, nous venons de tester 10 fonctionnalités en moins de 26 secondes — le même résultat que celui déjà montré en haut de cet article.
Pour aller plus loin (passez cette section si vous voulez juste l'utiliser)
- Pest utilise playwright ; c'est pour ça que notre Dockerfile est plus complexe, parce qu'on doit aussi installer NodeJS,
- Un truc vraiment cooooool : on n'a pas besoin de
wait_forun changement sur la page, Pest le fait pour nous. Par exemple, avec Cypress et Behat, quand on clique sur un bouton, on doit attendre que, par exemple, le script Ajax de la page se soit déclenché et que leDOM elementsoit chargé. Avec Pest, on ne doit pas s'en soucier et c'est une fonctionnalité géniale ! - On doit aussi créer un utilisateur spécifique dans l'image Docker pour correspondre à notre utilisateur local. C'est parce que, quand un test échoue, Pest (qui tourne dans Docker) va créer une image et celle-ci doit être créée sur notre machine hôte avec nos UID/GID,
- L'action
make updéfinie dans lemakefilemonte notre dossier localtestsdans le container. Ça veut dire que si on crée de nouveaux fichiers de test ou qu'on modifie les existants, ils seront immédiatement synchronisés avec le container en cours d'exécution ; rien de spécial à faire ici, - Pour réduire le besoin de fichiers de configuration, une variable système
WEBSITEa été définie (voir lemakefile). Cette variable sera créée dans le container. Un script PHP y fera ensuite référence comme ceci :getenv('WEBSITE'). - Il existe un flag
--parallelpour Pest. Si vous voulez exécuter vos différents fichiers de test en parallèle, éditez lemakefile, cherchezdocker exec -it demo_pest sh -c "WEBSITE=$(WEBSITE) vendor/bin/pest"et remplacez pardocker exec -it demo_pest sh -c "WEBSITE=$(WEBSITE) vendor/bin/pest --parallel"
- docker exec -it demo_pest sh -c "WEBSITE=$(WEBSITE) vendor/bin/pest"+ docker exec -it demo_pest sh -c "WEBSITE=$(WEBSITE) vendor/bin/pest --parallel"Cette modification ajoute le flag --parallel à la commande pest qui s'exécute dans un conteneur Docker. Cela permet aux tests de s'exécuter en parallèle au lieu de séquentiellement, ce qui accélère le temps d'exécution global de la suite de tests.
Cet article fait partie de la série Tests fonctionnels :
Chargement des commentaires…




