Chapitre 11.4 — Behat et CI/CD
Partie 11 : Niveau expert — Chapitre 4/8 Public : dev ayant des tests unitaires (11.3) ; il ajoute les tests d’acceptation et automatise tout en CI. Version de référence : Moodle 5.2 (Behat, Selenium/Chrome,
moodle-plugin-ci, GitHub Actions).
⏱️ TL;DR
- Behat teste le comportement de bout en bout dans un vrai navigateur (Gherkin :
Given/When/Then), là où PHPUnit teste des unités.- Initialisation :
admin/tool/behat/cli/init.php, un site Behat dédié, un driver navigateur (Selenium/Chrome), lancé dans moodle-docker.- On écrit des
.featureen Gherkin, en s’appuyant d’abord sur la riche bibliothèque de steps du core (login, création de cours, navigation…). Steps custom danstests/behat/behat_*.phpsi besoin.- Le tag
@javascriptexécute le scénario dans un vrai navigateur (nécessaire pour l’AJAX/JS) ; sans lui, un driver « headless » plus rapide (sans JS).moodle-plugin-ciest l’outil officiel : il orchestre lint, phpcs (moodle-cs), phpdoc, mustache lint, grunt, phpunit et behat sur une matrice PHP × Moodle × base de données, en GitHub Actions.- La CI est le filet automatique : chaque push/PR est vérifié contre plusieurs versions → on attrape les régressions avant de casser la prod ou de rater un upgrade.
🎯 Objectifs
- Écrire un test Behat en Gherkin en réutilisant les steps du core.
- Comprendre
@javascriptet l’exécution navigateur. - Créer un step custom dans un plugin.
- Configurer une CI
moodle-plugin-cicomplète en GitHub Actions. - Comprendre la matrice de test et les stratégies de déploiement continu.
1. Behat vs PHPUnit : deux filets complémentaires
- PHPUnit (11.3) teste des unités : « le manager calcule-t-il la bonne note ? ». Rapide, isolé, sans navigateur.
- Behat teste le comportement utilisateur de bout en bout : « quand un enseignant ajoute une activité et qu’un étudiant coche tous les items, sa note apparaît-elle dans le carnet ? ». Lent, réaliste, dans un vrai navigateur.
Les deux sont complémentaires : PHPUnit pour la logique, Behat pour les parcours (formulaires, navigation, JS, intégration). Un plugin mûr a les deux.
💡 Pour un dev React : Behat est votre Playwright/Cypress (tests end-to-end pilotant un navigateur), PHPUnit est votre Jest/Vitest (unités). Gherkin est le langage de scénario (à la Cucumber). Même pyramide de tests : beaucoup d’unitaires rapides, quelques E2E ciblés sur les parcours critiques.
2. Gherkin et la bibliothèque de steps du core
Un test Behat est un fichier .feature en Gherkin — des phrases Given (contexte), When (action), Then (vérification) :
# public/local/todolist/tests/behat/todolist.feature
@local @local_todolist
Feature: Personal to-do list
In order to organise my work
As a user
I need to add and complete tasks
Background:
Given the following "users" exist:
| username | firstname | lastname |
| alice | Alice | Doe |
And I log in as "alice"
@javascript
Scenario: Add a task and mark it done
When I visit "/local/todolist/index.php"
And I click on "Add a task" "link"
And I set the field "Task" to "Read chapter 11"
And I press "Save changes"
Then I should see "Read chapter 11"
When I click on "Mark as done" "link"
Then I should see "Read chapter 11" in the ".list-group-item-success" "css_element"Le point clé : la richesse des steps du core. Moodle fournit des centaines de steps prêts à l’emploi (I log in as, the following "courses" exist, I set the field, I should see, I click on … "…", I navigate to … in current page administration…). Avant d’écrire un step custom, cherchez s’il existe : la plupart des actions courantes sont déjà couvertes.
⚠️ Piège : réinventer des steps qui existent déjà dans le core. Les steps
the following "users"/"courses"/"activities" existcréent des fixtures déclarativement (comme le data generator, mais en Gherkin). Parcourez la liste des steps disponibles (admin/tool/behat/cli/util.phpgénère la doc des steps, ou l’éditeur Behat) avant d’écrire du PHP.
3. @javascript et l’exécution navigateur
Le tag @javascript décide du driver :
- Sans
@javascript: un driver « headless » (sans navigateur JS, type Goutte/BrowserKit) — rapide, mais ne teste pas le JavaScript (pas d’AJAX, pas de modales, pas decore/reactive). - Avec
@javascript: un vrai navigateur (Chrome via Selenium/WebDriver) — plus lent, mais teste le JS réel (modales, drag&drop,core/ajax, React…).
Règle : mettez @javascript dès que le scénario dépend du JS (la plupart des interactions modernes de Moodle). Réservez le mode sans-JS aux parcours purement serveur (navigation de liens, formulaires POST classiques) pour gagner en vitesse.
⚠️ Piège : un scénario qui clique sur un bouton ouvrant une modale AJAX (ch. 7.3) sans
@javascriptéchoue (la modale ne se charge pas). Symptôme classique : « l’élément n’existe pas ». Ajoutez@javascript. Inversement, tout passer en@javascriptralentit inutilement la suite.
4. Initialiser et lancer Behat (moodle-docker)
# 1. Config : $CFG->behat_prefix, behat_dataroot, behat_wwwroot dans config.php
# 2. Selenium/Chrome tourne (fourni par moodle-docker)
# 3. Initialiser le site Behat
php admin/tool/behat/cli/init.php
# 4. Lancer les tests d'un plugin (par tag)
vendor/bin/behat --tags=@local_todolist
# 5. Lancer un seul scénario
vendor/bin/behat local/todolist/tests/behat/todolist.featuremoodle-docker (Partie 4) fournit les conteneurs Selenium + navigateur, ce qui évite d’installer Java/Selenium localement. init.php prépare un site Behat isolé (base et dataroot dédiés, comme PHPUnit). À rejouer quand les features/steps changent structurellement.
⚠️ Piège des tests flaky : les tests navigateur sont sensibles au timing (élément pas encore chargé). Moodle fournit des steps d’attente (
I wait until "…" exists, gestion implicite des attentes AJAX). N’utilisez pas desleepfixes : préférez les attentes conditionnelles du core. Un test flaky non traité érode la confiance dans toute la suite (on finit par ignorer les échecs).
5. Un step custom
Quand aucun step du core ne convient, on en écrit dans tests/behat/behat_<component>.php :
<?php
// public/local/todolist/tests/behat/behat_local_todolist.php
// (hors namespace ; chargé par le harnais Behat)
require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
class behat_local_todolist extends behat_base {
/**
* @Given /^the user "([^"]*)" has a task "([^"]*)"$/
*/
public function the_user_has_a_task(string $username, string $taskname): void {
global $DB;
$user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
\local_todolist\manager::create($user->id, $taskname);
}
}Utilisé ensuite en Gherkin : Given the user "alice" has a task "Prepare fixtures". Un step custom est du PHP qui prépare l’état ou vérifie une condition ; on l’écrit seulement quand le core ne couvre pas le besoin.
💡 Pour un dev React : un step custom Behat ≈ une commande Cypress custom (
Cypress.Commands.add) ou un helper Playwright : une brique réutilisable qui encapsule une action/vérification propre à votre domaine. On les garde minimalistes et on privilégie les steps du framework.
6. La CI avec moodle-plugin-ci
moodle-plugin-ci est l’outil officiel qui exécute, en une commande, toute la batterie de vérifications qu’attend le plugin directory (11.8) et que la communauté utilise. En GitHub Actions :
# .github/workflows/ci.yml
name: Moodle plugin CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env: { POSTGRES_USER: postgres, POSTGRES_PASSWORD: postgres }
ports: ['5432:5432']
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
strategy:
fail-fast: false
matrix:
include:
- { php: '8.3', moodle-branch: 'MOODLE_502_STABLE', database: pgsql }
- { php: '8.3', moodle-branch: 'MOODLE_503_STABLE', database: pgsql }
steps:
- uses: actions/checkout@v4
with: { path: plugin }
- uses: shivammathur/setup-php@v2
with: { php-version: ${{ matrix.php }}, extensions: pgsql, gd, zip, intl }
- name: Install moodle-plugin-ci
run: |
composer create-project moodlehq/moodle-plugin-ci ci ^4 --no-interaction
echo "$(pwd)/ci/bin" >> $GITHUB_PATH
- name: Install Moodle + deps
run: moodle-plugin-ci install --plugin ./plugin --db-host=127.0.0.1
env:
DB: ${{ matrix.database }}
MOODLE_BRANCH: ${{ matrix.moodle-branch }}
- run: moodle-plugin-ci phplint
- run: moodle-plugin-ci phpcs --max-warnings 0 # moodle-cs
- run: moodle-plugin-ci phpmd
- run: moodle-plugin-ci phpdoc
- run: moodle-plugin-ci validate
- run: moodle-plugin-ci savepoints
- run: moodle-plugin-ci mustache
- run: moodle-plugin-ci grunt
- run: moodle-plugin-ci phpunit --coverage-text
- run: moodle-plugin-ci behat --profile chromeCe que chaque étape vérifie :
| Étape | Vérifie |
|---|---|
phplint | pas d’erreur de syntaxe PHP |
phpcs | coding style Moodle (moodle-cs) — zéro warning |
phpdoc | documentation des classes/fonctions |
validate | cohérence du plugin (version.php, lang, etc.) |
savepoints | les upgrade savepoints (ch. 6.3) sont corrects |
mustache | lint des templates (ch. 7.2) |
grunt | le JS AMD est compilé et à jour |
phpunit | les tests unitaires (11.3) |
behat | les tests d’acceptation |
La matrice (strategy.matrix) rejoue tout contre plusieurs versions de Moodle × PHP × bases de données — c’est ainsi qu’on garantit la compatibilité annoncée dans version.php (supported, ch. 6.7) et qu’on attrape les ruptures d’un futur Moodle avant sa sortie.
⚠️ Piège : lancer la CI sur une seule combinaison. Un plugin peut passer en PostgreSQL 5.2 et casser en MySQL 5.3. La matrice est le cœur de la valeur : elle teste ce que vous promettez de supporter. Alignez la matrice sur votre champ
supported(ch. 6.7).
7. Du CI au CD : déploiement continu
La CI vérifie ; le CD déploie. Pour une instance Moodle (pas un plugin publié) :
- Releases automatisées vers le plugin directory (11.8) : un workflow qui, sur un tag, publie la nouvelle version (via l’API du directory).
- Déploiement d’instance : sur validation CI, un pipeline applique la procédure du chapitre 6.7 (backup → mode maintenance → déploiement du code taggé →
admin/cli/upgrade.php→ purge → maintenance off), idéalement sur une préproduction d’abord, puis production. - Stratégies réalistes : peu d’instances font du « déploiement continu » pur (le mode maintenance interrompt le service) ; on préfère des fenêtres planifiées, des préproductions miroir, et des tests de fumée post-déploiement.
⚠️ Piège : automatiser un déploiement en production sans préproduction ni backup automatique. Une migration de schéma ratée sur la prod d’étudiants est un incident majeur (ch. 6.7). Le CD Moodle raisonnable = CI stricte → préprod automatique → prod sur validation humaine, avec backup et mode maintenance scriptés.
8. Synthèse
Behat teste les parcours dans un vrai navigateur (Gherkin, steps core d’abord, @javascript pour le JS), complément des unités PHPUnit. moodle-plugin-ci automatise tout (lint, coding style, mustache, grunt, phpunit, behat) sur une matrice de versions en GitHub Actions — le filet qui garantit la compatibilité promise et attrape les régressions avant la prod. Le CD raisonnable ajoute préproduction, backup et validation humaine à la procédure du chapitre 6.7. Avec la sécurité (11.1), la performance (11.2) et les tests (11.3-11.4), votre code est sûr, rapide et durable — reste la conformité (RGPD, IA) et l’exploitation, chapitres suivants.
✏️ Exercices
Exercice 1 — Behat ou PHPUnit ? Pour chaque test : (a) « le calcul de note = cochés/total × max » ; (b) « un enseignant ajoute une activité via le formulaire et un étudiant la termine, la note apparaît » ; (c) « une modale d’ajout s’ouvre et enregistre en AJAX ».
✅ Solution
(a) PHPUnit (logique unitaire du calcul, pas de navigateur) ; (b) Behat (parcours de bout en bout : formulaire, navigation, carnet de notes — intégration) ; (c) Behat avec @javascript (interaction JS/AJAX dans un vrai navigateur). Règle : logique → PHPUnit ; parcours/UI/JS → Behat (et @javascript dès qu’il y a du JS).
Exercice 2 — Step manquant ?
Vous voulez créer trois utilisateurs et un cours dans un Background. Faut-il écrire des steps custom ?
✅ Solution
Non : le core fournit des steps de fixtures déclaratives — Given the following "users" exist: et Given the following "courses" exist: (avec des tables Gherkin de données). On les réutilise. On n’écrit un step custom (tests/behat/behat_*.php) que pour un état spécifique à votre plugin non couvert (ex. « l’utilisateur X a une tâche Y »). Toujours chercher le step core d’abord.
Exercice 3 — L’élément n’existe pas. Un scénario clique sur un bouton qui ouvre une modale, et Behat échoue « élément introuvable ». Le scénario n’a pas de tag. Diagnostic ?
✅ Solution
La modale se charge en AJAX/JS (ch. 7.3), or sans le tag @javascript le scénario tourne avec un driver sans JavaScript : la modale ne s’ouvre jamais, donc son contenu « n’existe pas ». Correctif : ajouter @javascript au scénario pour l’exécuter dans un vrai navigateur (Chrome/Selenium). Vérifier aussi qu’on utilise des attentes du core (pas de sleep) pour laisser l’AJAX se terminer.
Exercice 4 — Matrice de CI.
Votre version.php déclare supported = [502, 503]. Comment doit être la matrice de CI, et pourquoi ?
✅ Solution
La matrice doit au moins couvrir Moodle 5.2 et 5.3 (les branches MOODLE_502_STABLE et MOODLE_503_STABLE), avec la (les) version(s) de PHP supportée(s) et idéalement plusieurs bases (PostgreSQL, MySQL/MariaDB). Pourquoi : supported promet ces versions ; la CI doit prouver cette promesse. Un plugin peut passer en 5.2/pgsql et casser en 5.3/mysql — seule la matrice l’attrape. Alignez la matrice sur supported (ch. 6.7).
Exercice 5 — CD prudent.
Un dev veut déployer automatiquement en production à chaque merge sur main. Quels garde-fous imposez-vous ?
✅ Solution
Garde-fous : (1) CI verte obligatoire (matrice complète) avant tout déploiement ; (2) déploiement d’abord sur une préproduction miroir, avec tests de fumée ; (3) backup automatique (base + moodledata + code) avant la prod ; (4) mode maintenance scripté pendant la migration (ch. 6.7) ; (5) validation humaine avant la prod (le mode maintenance interrompt le service — pas de déploiement continu « aveugle » sur une prod d’étudiants) ; (6) tests de fumée post-déploiement + plan de rollback. Le CD Moodle raisonnable n’est pas « merge = prod » mais « CI → préprod → validation → prod scriptée ».
🧠 Quiz de révision
Q1. Qu’est-ce que Behat teste, par opposition à PHPUnit ?
Réponse
Q2. Que fait le tag @javascript et quand est-il nécessaire ?
Réponse
core/ajax, React). Sans lui, ces interactions échouent.Q3. Pourquoi privilégier les steps du core avant d’en écrire ?
Réponse
behat_*.php) que pour un état spécifique non couvert.Q4. Que fait moodle-plugin-ci et pourquoi la matrice est-elle centrale ?
Réponse
moodle-cs, phpdoc, validate, savepoints, mustache, grunt, phpunit, behat) en GitHub Actions. La matrice (PHP × Moodle × base) est centrale car elle prouve la compatibilité promise (supported) : un plugin peut passer sur une combinaison et casser sur une autre.Q5. À quoi ressemble un CD Moodle raisonnable ?
Réponse
upgrade.php + purge, ch. 6.7). Pas de « merge = prod » aveugle, car la migration interrompt le service et un échec sur la prod d’étudiants est critique.Chapitre suivant : 05 — La Privacy API : Moodle et le RGPD — les obligations légales appliquées à un LMS, l’architecture de la Privacy API (providers de métadonnées, de requêtes, de listes d’utilisateurs), l’implémentation complète (null_provider, export, suppression), le traitement des data requests, et les exigences du plugin directory.