ps_onepagecheckout : exécuter les tests PHPUnit avec Docker et PrestaShop

Guide pratique pour lancer des tests d’intégration reproductibles de ps_onepagecheckout : conteneurs PHP+MariaDB, bootstrap PrestaShop, réinitialisation DB et astuces anti‑flaky.

Deux écrans montrant du code pour des tests PHPUnit avec Docker et PrestaShop.

Table des matières :

  1. Contexte : pourquoi isoler les tests PHPUnit de ps_onepagecheckout avec Docker
  2. Ce que PHPUnit valide réellement dans un module PrestaShop (et ce qu’il ne validera jamais)
  3. Pré-requis côté repo : composer.json, phpunit.xml et bootstrap PrestaShop propre
  4. docker-compose.yml pour PHPUnit : un conteneur PHP CLI + une DB (pas besoin de Nginx)
  5. Image PHP de test : extensions PrestaShop, Composer, et choix Xdebug vs PCOV
  6. Installer PrestaShop automatiquement (et réinitialiser la DB) : la partie qui casse le plus souvent
  7. Exécuter PHPUnit pour ps_onepagecheckout : commandes utiles, artefacts et anti-flakiness
  8. CI/CD : reproduire la commande locale et aligner dev/staging/prod (sans fantasmer)
  9. Limites connues du core PrestaShop en environnement de test (et contournements pragmatiques)

Contexte : pourquoi isoler les tests PHPUnit de ps_onepagecheckout avec Docker

ps_onepagecheckout n’est pas un module « pur PHP » : il s’adosse à l’état global de PrestaShop (singleton Context, caches statiques, Configuration, base MySQL, système de hooks, templates). Résultat : exécuter des tests PHPUnit sur une machine de dev « sale » (extensions PHP différentes, php.ini bricolé, DB partagée, droits fichiers aléatoires) produit des faux négatifs et des tests instables. Docker sert ici de garde-fou : même image PHP, même MariaDB/MySQL, même arbre PrestaShop, et une réinstallation reproductible.

Dans un module checkout, les causes d’instabilité sont souvent très concrètes (et rarement « PHPUnit lui‑même ») :

  • État persistant : tables SQL non nettoyées (paniers, règles panier, adresses), caches Smarty/Symfony, fichiers dans var/.
  • Dépendances à la machine : extensions PHP manquantes (intl/zip/gd), locales, timezone, limites mémoire.
  • Effets de bord : envoi de mails, génération de PDF, accès réseau (webhooks), hooks exécutés par d’autres modules installés « par hasard ».

Le but n’est pas de « dockeriser pour dockeriser », mais de rendre une suite PHPUnit déterministe : on veut pouvoir lancer les tests du module sur n’importe quel poste (Linux/macOS/Windows WSL2) et obtenir le même résultat, avec un coût d’onboarding proche de zéro. Le gain devient évident dès que vous devez valider une MR/PR : au lieu d’expliquer une stack locale, vous fournissez un docker compose up + une commande, et vous supprimez 80% des « chez moi ça marche ».

Docker formalise très bien cette notion d’instance exécutable. La doc officielle le résume sans ambiguïté : « A container is a runnable instance of an image » (Docker Documentation — What is a container?). En pratique, ça vous permet d’aligner PHP, extensions, OS users, et dépendances natives (libzip, ICU…) sur une base stable — ce que PrestaShop rend pénible à maintenir à la main.

Ce que PHPUnit valide réellement dans un module PrestaShop (et ce qu’il ne validera jamais)

Avant d’écrire du YAML, clarifiez la cible : tests unitaires (sans DB, sans kernel) vs tests d’intégration (PrestaShop initialisé, DB disponible, services Symfony éventuels). Sur PrestaShop, les « vrais » bugs de checkout se trouvent rarement dans des fonctions isolées ; ils se déclenchent via un hook, une surcharge, une règle de panier, une taxe, un mode multiboutique, etc. Donc si votre suite ne fait que du unit test sans bootstrap PrestaShop, elle donne une illusion de couverture.

PHPUnit reste l’outil standard côté PHP. Sa définition est explicite : « PHPUnit is a programmer-oriented testing framework for PHP » (PHPUnit Manual). À partir de là, le vrai sujet PrestaShop est le bootstrap : comment fournir à PHPUnit un contexte stable (autoload, constantes _PS_ROOT_DIR_, DB, Context::getContext()) sans polluer la suite avec des effets de bord.

Pour cadrer le périmètre, une grille simple aide à choisir quoi tester (et avec quel coût) :

Type de test Objectif Dépendances Exemple pertinent pour ps_onepagecheckout
Unitaire Valider une logique pure Aucune DB / pas de PrestaShop Normalisation de données, validation d’un DTO, calcul isolé
Intégration (module + core) Valider que le module « parle bien » au core DB + PrestaShop bootstrappé Création d’adresse, recalcul panier, application d’une règle panier via hooks
E2E (hors scope ici) Valider parcours client complet Navigateur + JS + thème + API paiements Affichage erreurs, UX, compatibilité thème, évènements JS

Il faut aussi assumer ce que PHPUnit ne vous donnera pas : il ne simule pas un navigateur, ne teste pas les comportements JS du checkout, et ne couvre pas la compatibilité thème. Pour ça, vous êtes sur du Playwright/Cypress + environnements front. Dans cet article on reste sur un périmètre backend et intégration PrestaShop, parce que c’est là que ps_onepagecheckout casse généralement (règles panier, adresses, transporteurs, paiements, hooks).

Pré-requis côté repo : composer.json, phpunit.xml et bootstrap PrestaShop propre

Sur un module PrestaShop moderne, l’état minimal viable est : un composer.json au niveau du module, un dossier tests/, et un phpunit.xml.dist qui pointe vers un bootstrap. Si vous partez du dépôt officiel / d’un fork de ps_onepagecheckout, commencez par vous aligner avec le workflow de build (assets + vendor) décrit ici : PrestaShop ps_onepagecheckout : prérequis, installation et workflow de build. Le point critique : ne mélangez pas les vendors du module avec ceux du core PrestaShop.

Côté composer.json, l’idée est de garder PHPUnit et les outils de test en require-dev, et de figer des versions compatibles avec la branche PrestaShop ciblée. Un exemple minimal (à adapter) :

{
  "require-dev": {
    "phpunit/phpunit": "^10.0"
  },
  "autoload-dev": {
    "psr-4": {
      "Tests\\": "tests/"
    }
  }
}

Côté phpunit.xml.dist, cherchez surtout à éviter les surprises liées à l’environnement : variables, erreurs converties en exceptions si nécessaire, et bootstrap stable. Un squelette minimal (là encore, à adapter) :

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php" colors="true">
  <php>
    <ini name="date.timezone" value="UTC"/>
  </php>
  <testsuites>
    <testsuite name="Module">
      <directory>tests</directory>
    </testsuite>
  </testsuites>
</phpunit>

Côté bootstrap, la règle est simple : vous devez initialiser PrestaShop comme un front controller, mais en coupant tout ce qui rend les tests non déterministes (cache, mails, appels réseau). Typiquement : forcer $_SERVER (host, scheme), définir _PS_MODE_DEV_, désactiver l’envoi de mails, et surtout créer/retirer une base de données de test à chaque exécution (ou à minima par suite). PrestaShop n’a pas été conçu pour l’isolation de tests : beaucoup de classes lisent directement Configuration ou Context sans injection de dépendances ; vous devez donc « nettoyer derrière vous ».

Checklist pragmatique (souvent oubliée) pour un bootstrap « test-friendly » :

  • Fixer $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'], $_SERVER['REQUEST_METHOD'] si le code passe par des helpers HTTP.
  • Fixer timezone/locale (au minimum timezone).
  • Désactiver ce qui « sort » de la suite : mails, appels HTTP, écriture de fichiers non nécessaires.
  • Prévoir un mécanisme de reset entre tests : DB + caches + Context.

Enfin, fixez vos versions. En 2026, vous n’avez plus d’excuse pour exécuter un module sur une version PHP non supportée par la branche cible. Pour PrestaShop 9, le sujet est même piégeux (docs parfois incohérentes) : gardez un œil sur PrestaShop 9 : versions PHP recommandées et incohérences de documentation. Dans les exemples ci-dessous, je pars sur PrestaShop 8.1.7 et PHP 8.2 (et PHPUnit 10), parce que c’est un socle courant et stable.

docker-compose.yml pour PHPUnit : un conteneur PHP CLI + une DB (pas besoin de Nginx)

Pour exécuter PHPUnit, vous n’avez pas besoin d’un serveur web. Un conteneur PHP CLI avec les extensions nécessaires + un service MariaDB/MySQL suffit. Évitez de reproduire votre stack prod (Nginx/Varnish/Redis) tant que l’objectif est uniquement de faire tourner des tests. À l’inverse, si vos tests touchent du cache ou des queues, ajoutez les services explicitement, pas « au cas où ».

Voici une base compose minimale. Elle suppose que vous montez un checkout PrestaShop dans ./prestashop/ et votre module dans ./modules/ps_onepagecheckout/ (ou que le module est déjà dans prestashop/modules/). L’important est de maîtriser les volumes et d’éviter les permissions root qui ruinent les workspaces.

Petit ajout très utile en pratique : un volume de cache Composer (sinon chaque composer install repart de zéro), et des UID/GID paramétrables pour Linux (sur macOS/WSL2, c’est généralement moins critique mais ça reste propre).

services:
  db:
    image: mariadb:10.11
    environment:
      MARIADB_ROOT_PASSWORD: root
      MARIADB_DATABASE: prestashop_test
      MARIADB_USER: prestashop
      MARIADB_PASSWORD: prestashop
    command: ["--character-set-server=utf8mb4","--collation-server=utf8mb4_unicode_ci"]
    healthcheck:
      test: ["CMD", "mariadb-admin", "ping", "-proot"]
      interval: 2s
      timeout: 2s
      retries: 30

  php:
    build:
      context: .
      dockerfile: Dockerfile.phpunit
      args:
        UID: 1000
        GID: 1000
    working_dir: /var/www/html
    environment:
      PS_DOMAIN: localhost
      DB_SERVER: db
      DB_NAME: prestashop_test
      DB_USER: prestashop
      DB_PASSWD: prestashop
      PHP_MEMORY_LIMIT: 1024M
      COMPOSER_HOME: /tmp/composer
    volumes:
      - ./prestashop:/var/www/html:delegated
      - composer-cache:/tmp/composer
    depends_on:
      db:
        condition: service_healthy

volumes:
  composer-cache:

Si vous n’êtes pas à l’aise avec Compose v2, ou si vous voulez aller plus loin (réseaux, profils, dépendances conditionnelles), vous avez un rappel utile ici : Docker Compose : orchestrer des services multi-conteneurs en production. Même si on parle de « production » dans le titre, les mécaniques healthcheck, profiles, depends_on et la gestion des volumes sont exactement les mêmes pour des tests.

Image PHP de test : extensions PrestaShop, Composer, et choix Xdebug vs PCOV

Le conteneur PHP doit coller aux exigences PrestaShop (pdo_mysql, intl, gd, zip, xml, mbstring…). Ne « découvrez » pas une extension manquante en CI : faites échouer le build Docker dès le départ. Exemple de Dockerfile.phpunit (PHP 8.2 CLI) :

FROM php:8.2-cli-bookworm

ARG UID=1000
ARG GID=1000

RUN apt-get update && apt-get install -y --no-install-recommends \
    git unzip libicu-dev libzip-dev libpng-dev libjpeg62-turbo-dev libfreetype6-dev \
    default-mysql-client \
  && docker-php-ext-configure gd --with-freetype --with-jpeg \
  && docker-php-ext-install -j$(nproc) intl pdo_mysql zip gd opcache \
  && rm -rf /var/lib/apt/lists/*

# Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

# Utilisateur non-root pour éviter de pourrir les permissions du repo
RUN groupadd -g ${GID} app && useradd -m -u ${UID} -g ${GID} app
USER app

ENV COMPOSER_HOME=/tmp/composer

Deux détails qui évitent des heures perdues :

  • default-mysql-client : indispensable si vous voulez faire un mysql/mariadb dans le conteneur (drop DB, import SQL, debug). Sans ça, vous finissez à bricoler un conteneur « admin » séparé.
  • user non-root + UID/GID : sinon, vous allez créer des fichiers appartenant à root dans votre repo (typiquement vendor/), et l’équipe va vous détester.

Pour la couverture, vous avez deux approches : Xdebug (simple mais plus lent) ou PCOV (plus rapide, plus limité). Sur des suites longues, Xdebug peut facilement multiplier le temps d’exécution (surtout si la suite fait beaucoup d’I/O). Si votre objectif est uniquement de faire du --coverage-text ponctuellement, Xdebug est OK. Si vous voulez une couverture systématique en CI, PCOV devient souvent plus pragmatique.

Un résumé opérationnel (sans dogme) :

  • Xdebug : bon pour déboguer et générer de la couverture « à la demande » ; à activer uniquement quand nécessaire.
  • PCOV : bon pour générer de la couverture rapidement en CI ; moins orienté debug interactif.

Si vous partez sur Xdebug, faites-le proprement (CLI + web) et documentez votre config. Vous avez deux articles utilisables comme référence interne : Xdebug : installer et activer Xdebug 3 pour PHP CLI et web et Xdebug : configurer le débogage PHP dans Visual Studio Code. Le point important : en environnement de test, activez Xdebug uniquement quand vous en avez besoin (sinon vous dégradez les temps de feedback et vous finissez par moins lancer les tests).

Installer PrestaShop automatiquement (et réinitialiser la DB) : la partie qui casse le plus souvent

Exécuter PHPUnit « juste » sur le module est rarement suffisant : beaucoup de tests d’intégration exigent une boutique installée, une DB cohérente et un répertoire var/ accessible en écriture. Le core PrestaShop n’est pas tendre : si var/cache n’est pas writable, vous aurez des erreurs cryptiques ; si la DB contient des résidus (tables custom, anciennes migrations), vous aurez des faux positifs. Donc l’approche robuste est : drop + install sur une base dédiée.

Un pattern qui marche bien : vous considérez la base comme jetable, et vous la recréez avant l’installation (ça évite les « il restait des tables… »). Par exemple, avant de lancer index_cli.php, vous pouvez forcer un reset :

docker compose run --rm php bash -lc '
  mariadb -h"$DB_SERVER" -u"$DB_USER" -p"$DB_PASSWD" -e "DROP DATABASE IF EXISTS \`$DB_NAME\`; CREATE DATABASE \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
'

Sur PrestaShop 8.1.x, l’install CLI reste le chemin le plus direct (dans le conteneur php) :

# Attention : détruit la base de test.
docker compose run --rm php bash -lc '
  cd /var/www/html && \
  php install/index_cli.php \
    --domain=localhost \
    --db_server=$DB_SERVER --db_name=$DB_NAME --db_user=$DB_USER --db_password=$DB_PASSWD \
    --prefix=ps_ \
    [email protected] --password=admin123 \
    --language=fr --country=fr --name="Test Shop" \
    --ssl=0 --all_languages=0 --newsletter=0 --send_email=0
'

Deux points « terrain » en contexte FR/EU (utile pour un checkout) :

  • --language=fr --country=fr : vous testez par défaut un contexte proche des boutiques françaises (format d’adresse, pays, TVA), ce qui réduit les surprises.
  • Même si vous fixez country=fr, testez au moins un scénario « UE » (ex. livraison Belgique) dans la suite d’intégration : beaucoup de bugs checkout se déclenchent sur TVA / transport / pays.

Après installation, pour vos tests d’intégration, il est souvent nécessaire d’installer et activer le module (et parfois de désactiver des modules parasites). Sur PrestaShop 8, le bin/console peut aider à automatiser (selon la version du core), sinon vous retombez sur les APIs de module depuis un script de test. L’objectif reste identique : zéro manipulation manuelle entre deux runs.

Sur PrestaShop 9 (Symfony 6.4), vous pouvez aussi piloter davantage via bin/console (installation de modules, nettoyage cache, etc.). Mais ne vous racontez pas d’histoire : PrestaShop garde beaucoup d’état global, et les tests deviennent flaky si vous ne réinitialisez pas au minimum Context, le cache et certaines tables métiers. Si vous cherchez une méthode de test « rapide » qui évite la réinstallation, elle existe (transactions DB + rollback), mais elle demande une discipline stricte sur les tables touchées — discipline que la plupart des modules checkout n’ont pas.

Exécuter PHPUnit pour ps_onepagecheckout : commandes utiles, artefacts et anti-flakiness

Une fois PrestaShop installé dans le volume, vous pouvez installer les dépendances Composer du module et lancer PHPUnit. L’idée est de toujours exécuter composer install dans un contexte identique (même PHP, mêmes extensions). Exemple (en supposant que le module est dans modules/ps_onepagecheckout) :

# Installer les vendors du module
docker compose run --rm php bash -lc '
  cd /var/www/html/modules/ps_onepagecheckout && \
  composer install --no-interaction --prefer-dist
'

# Lancer la suite
docker compose run --rm php bash -lc '
  cd /var/www/html/modules/ps_onepagecheckout && \
  ./vendor/bin/phpunit -c phpunit.xml.dist
'

En quotidien, quelques options PHPUnit améliorent fortement le signal :

  • --stop-on-failure : utile en local (feedback rapide).
  • --fail-on-warning / --fail-on-risky : utile en CI pour éviter que des tests « passent » alors qu’ils sont en réalité douteux.
  • --filter : pour isoler un scénario de régression (ex. un test lié au recalcul transporteur).

Pour une exécution exploitable en CI, produisez des artefacts : JUnit XML (pour annoter les jobs), éventuellement Clover/LCOV pour la couverture. Exemple d’options : --log-junit var/phpunit/junit.xml --coverage-clover var/phpunit/clover.xml. Ne stockez pas ça dans un volume partagé de prod : gardez-le dans un dossier ignoré par Git (var/phpunit/).

La stabilité dépend surtout de trois points, pas de Docker : (1) timezone/locale non fixées, (2) DB non nettoyée entre tests, (3) dépendances au temps (date du jour, expirations panier). Fixez date.timezone=UTC, évitez new DateTime() sans injection, et faites un reset DB par suite ou par classe (au minimum). Si vous suspectez une fuite mémoire ou des effets de cache Symfony/Smarty, exécutez un run avec --process-isolation pour valider, puis revenez en mode standard (c’est lent mais diagnostique bien).

Un mini-scenario typique de flakiness checkout (vu en production) : un test crée un panier avec une règle panier « livraison gratuite », puis un second test s’attend à des frais de port > 0, mais la règle panier n’a pas été supprimée (ou le cache de règles n’a pas été purgé). Docker n’y changera rien : le remède est un nettoyage explicite (DB et/ou objets CartRule) et une suite qui ne dépend pas de l’ordre d’exécution.

CI/CD : reproduire la commande locale et aligner dev/staging/prod (sans fantasmer)

L’intérêt réel de « exécuter les tests PHPUnit avec Docker et PrestaShop » est la CI. Vous voulez que la commande en pipeline soit littéralement celle que vous lancez en local, pas une version approximative. Exemple de job GitHub Actions (principe identique sur GitLab CI) : build de l’image, démarrage DB, install PrestaShop, lancement PHPUnit. Le plus important : conserver le cache Composer entre runs et ne pas réinstaller 200 Mo de vendors à chaque commit.

Deux optimisations « low effort / high impact » côté CI :

  • Cache Composer : via volume Docker en local, et via cache CI (répertoire .composer / COMPOSER_CACHE_DIR) en pipeline.
  • Couches Docker : si votre CI supporte le cache de build, l’image PHP (extensions) ne devrait pas se reconstruire à chaque push.

Ce point rejoint un principe d’architecture largement cité : « Keep development, staging, and production as similar as possible » (The Twelve-Factor App — Dev/prod parity). Docker ne garantit pas la parité à lui seul, mais il réduit drastiquement l’écart (versions PHP, libs système, extensions). C’est particulièrement utile si votre hébergement cible est hétérogène (agence + infra client) ou si vous basculez entre VPS et cloud managé.

Si vous dimensionnez une machine dédiée aux builds/tests (self-hosted runner, VM de CI), évitez le sous-dimensionnement. Un checkout + install PrestaShop + suite PHPUnit peut saturer CPU et I/O, surtout avec bind-mount sur macOS. Pour cadrer le sujet infra, vous pouvez croiser ces références : VPS pour Docker : critères techniques et ressources recommandées et, côté choix d’offre, Comparatif hébergement PrestaShop (mutualisé / VPS / cloud managé). Le lien avec les tests est direct : si votre runner swap, vos temps de pipeline explosent et vous finissez par désactiver les tests — donc vous perdez l’objectif.

Limites connues du core PrestaShop en environnement de test (et contournements pragmatiques)

PrestaShop reste un socle historiquement monolithique avec beaucoup de statique. Même en 8/9, on se retrouve vite avec : Context global, caches en dur, constantes, et un couplage fort aux tables SQL. Ce n’est pas « mal » en soi, mais ça rend les tests d’intégration coûteux si vous voulez une isolation parfaite. Le contournement pragmatique est d’assumer une granularité : tests unitaires pour la logique pure, tests d’intégration pour les scénarios checkout critiques, et tests e2e pour le JS.

Concrètement, pour ps_onepagecheckout, je recommande de cibler en priorité des scénarios qui ont une valeur métier ET un historique de régressions : calcul de frais de port après changement d’adresse, application d’un code promo avec transporteur gratuit, bascule de pays avec TVA intracom, gestion des produits virtuels vs physiques, et cas multi-adresses si vous le supportez. Ce sont les zones où les hooks et règles panier interagissent, et où la moindre divergence de configuration casse en prod.

Un contournement simple (mais efficace) quand vous touchez à Context et aux singletons : forcer une remise à zéro entre certains tests, même si ce n’est pas « élégant ». Par exemple, sur des tests qui modifient pays/devise/langue, vous pouvez :

  • recréer un Context propre,
  • vider les caches applicatifs pertinents,
  • et, si nécessaire, isoler certains tests avec les annotations PHPUnit dédiées (au prix du temps d’exécution).

Enfin, ne négligez pas la discipline « opérationnelle » autour des tests : logs centralisés, runbook de dépannage, et politique de maintenance. Un module checkout touche au paiement : vous voulez savoir rapidement quand une mise à jour casse un flux. Pour cadrer ce périmètre (sans discours commercial), l’article PrestaShop maintenance : tarifs, niveaux de service et périmètre est utile parce qu’il liste ce qui relève d’un support/maintenance sérieux (monitoring, correctifs, environnements de pré-prod, etc.). Les tests automatisés ne remplacent pas tout, mais ils sont le premier filet anti-régression — à condition de tourner réellement, donc d’être exécutables partout via Docker.


À lire aussi