Retour au blog
Tests

PrestaFlow : fixtures et seed data reproductibles

PrestaEdit •
PrestaFlow : fixtures et seed data reproductibles

Décor

Plusieurs annexes précédentes ont contourné le sujet. L’annexe Tester un module tiers a couvert l’isolation des données au ras des pâquerettes : suffixe timestamp pour éviter les doublons, cleanup optionnel en fin de suite. Ça marche tant qu’on présuppose que la boutique a déjà ce qu’il faut — au moins un produit vendable, un mode de livraison configuré, une TVA active, un compte client valide.

Sur une boutique de démo Flashlight fraîche, la plupart de ces prérequis existent par défaut (produits, transporteurs, compte client de démo), mais les modules de paiement n’acceptent que le Royaume-Uni, le pays d’installation de l’image : une commande livrée en France ne trouve aucun moyen de paiement. C’est exactement le genre de prérequis qu’un init-script corrige (voir celui de psflowdemo, tests/flashlight-init/30-enable-payment.sh). Sur un projet un peu plus mûr — module qui teste un cas métier précis, boutique dont on veut contrôler chaque objet — il faut souvent semer plus. C’est le sujet de cette annexe.

Trois niveaux de fixtures, choisis selon la question qui compte : à quelle fréquence l’état doit-il être rechargé ?

  • Container-level — au premier démarrage du conteneur PS. Persiste tant que le conteneur vit.
  • Run-level — avant l’exécution de PrestaFlow, une fois par run CI.
  • Suite-level — avant chaque suite (ou groupe de suites).

Chacun a sa mécanique, son coût, et son bon cas d’usage.

Niveau 1 : Flashlight init-scripts

Le plus propre pour du seed statique : module à installer, config globale à poser, données de démo à charger. Vous versionnez le seed avec le code, il est appliqué automatiquement au premier démarrage du conteneur.

L’Action officielle expose l’input flashlight-init-scripts :

- uses: PrestaFlow/github-action@v2
  with:
    token: ${{ secrets.PRESTAFLOW_TOKEN }}
    projectId: 'pk_01ABCDEFGHIJKLMNOP'
    flashlight: true
    ps-version: '8.1.7'
    flashlight-init-scripts: ./tests/flashlight-init

Sous le capot, le dossier ./tests/flashlight-init est monté en /tmp/init-scripts (read-only) dans le conteneur. Les scripts qui s’y trouvent sont exécutés par ordre alphabétique, au premier démarrage du conteneur. Flashlight pose ensuite un verrou (/tmp/flashlight-init-scripts.lock) : un simple redémarrage du même conteneur ne les rejoue pas, sauf avec INIT_SCRIPTS_ON_RESTART=true. En CI, chaque run crée un conteneur neuf, donc la question ne se pose pas.

Exemple : les deux scripts du module fil rouge PrestaFlow/psflowdemo.

tests/flashlight-init/10-install-module.sh

#!/usr/bin/env bash
set -e

cd /var/www/html

# Le module vit à /var/www/html/modules/psflowdemo grâce à flashlight-mount: auto
php bin/console prestashop:module install psflowdemo

tests/flashlight-init/20-seed-config.sh

#!/usr/bin/env bash
set -e

cd /var/www/html

# Valeur par défaut connue et stable, sur laquelle les scénarios peuvent s'appuyer.
# L'installation la pose déjà ; on la fixe à nouveau au cas où le module
# changerait un jour sa valeur par défaut.
# prestashop:config n'existe qu'à partir de PrestaShop 8 : on saute l'étape en 1.7.
if php bin/console list prestashop 2>/dev/null | grep -q 'prestashop:config'; then
    php bin/console prestashop:config set PSFLOWDEMO_TITLE --value "Bienvenue sur notre boutique"
fi

Quand s’exécutent-ils, exactement ?

Le script de démarrage de Flashlight suit un ordre fixe : restauration du dump SQL, installation des modules de INSTALL_MODULES_DIR, init-scripts, puis démarrage de php-fpm et nginx, et enfin les post-scripts (POST_SCRIPTS_DIR, monté par défaut en /tmp/post-scripts).

Conséquence pratique : pendant un init-script, la base est prête et bin/console fonctionne, mais la boutique ne répond pas encore en HTTP. Pour tout ce qui a besoin d’une boutique servie, ou qui manipule des objets comme Shop ou ShopUrl (créer une seconde boutique, par exemple), utilisez un post-script. L’annexe PrestaShop Flashlight : des boutiques jetables pour tester en montre un complet.

Niveau 2 : step pré-run en CI

Pour du seed dynamique — quantité variable, dépendance à un input externe, contenu qui change entre runs. On ajoute un step de workflow entre “Wait for PrestaShop to be ready” et “Run PrestaFlow scenarios”, qui crée ce qu’il faut dans la boutique.

Ce niveau suppose le workflow « à la main » de l’article sur la CI, où vous démarrez vous-même le conteneur Flashlight (nommé ps). Avec l’Action, Flashlight démarre, exécute les suites et s’arrête dans une seule et même étape : il n’y a pas de place pour une étape intermédiaire.

Reste à choisir comment créer les objets. PrestaShop n’offre pas d’API d’écriture simple et commune à toutes les versions :

  • l’API d’administration de PrestaShop 9 (/admin-api/) demande un client API créé dans le BO, un jeton OAuth obtenu en client_credentials, et HTTPS (ou une option à désactiver en mode debug) ; elle n’existe pas en 1.7 ni en 8 ;
  • le webservice historique, disponible de 1.7 à 9, doit être activé et attend une clé, avec des corps XML pour l’écriture ;
  • docker exec dans le conteneur, lui, fonctionne partout : on charge PrestaShop et on passe par ses classes (Product, CartRule…), comme le ferait un module.

C’est la troisième voie qu’on retient. Un script PHP versionné, par exemple tests/ci-seed/seed-products.php :

<?php
// Crée SEED_COUNT produits actifs, en stock, dans la catégorie Accueil.
// Idempotent : un produit dont la référence existe déjà est ignoré.
chdir('/var/www/html');
require 'config/config.inc.php';

$count = (int) (getenv('SEED_COUNT') ?: 5);
$idHome = (int) Configuration::get('PS_HOME_CATEGORY');

for ($i = 1; $i <= $count; ++$i) {
    $reference = sprintf('PF-PAGINATION-%02d', $i);
    if (Product::getIdByReference($reference)) {
        continue;
    }

    $product = new Product();
    $product->reference = $reference;
    $product->name = [];
    $product->link_rewrite = [];
    foreach (Language::getIDs(false) as $idLang) {
        $product->name[$idLang] = 'Produit pagination ' . $i;
        $product->link_rewrite[$idLang] = 'produit-pagination-' . $i;
    }
    $product->price = 9.99;
    $product->id_category_default = $idHome;
    $product->active = true;
    $product->add();
    $product->addToCategories([$idHome]);
    StockAvailable::setQuantity((int) $product->id, 0, 100);

    echo "Produit {$reference} créé (id {$product->id})\n";
}

Et le step qui l’exécute, en envoyant le script sur l’entrée standard de php dans le conteneur :

- name: Seed 5 products for pagination test
  run: docker exec -i -e SEED_COUNT=5 ps php < tests/ci-seed/seed-products.php

- name: Run PrestaFlow scenarios
  # ...

Vérifié sur Flashlight 8.1.7 (produits créés, visibles en front dans la catégorie Accueil, second passage sans effet) et 9.0.0 (produits créés).

Utile pour les fixtures qui varient (nombre de produits, jeu de données extrait d’un système externe, fixtures alignées sur la date du run). Coût : un step de plus, et une dépendance aux classes PHP de PrestaShop, qui bougent peu mais bougent.

Niveau 3 : override de before() dans une TestsSuite

Pour du seed spécifique à une suite — un titre de module précis, un code promo qui n’existe que le temps du scénario, un utilisateur avec un panier abandonné qu’on veut relancer. On étend TestsSuite::before() dans la suite concernée, on appelle parent::before() d’abord, puis notre setup.

Exemple avec psflowdemo : la suite pose un titre à elle avant de démarrer, vérifie qu’il s’affiche, puis remet le titre par défaut.

<?php

namespace Tests\Suites;

use PrestaFlow\Library\Expects\Expect;
use PrestaFlow\Library\Tests\TestsSuite;

class DisplaySeededTitle extends TestsSuite
{
    private const SEEDED_TITLE = 'Titre posé par before()';

    public function before($headless = null, bool $getBrowser = true)
    {
        // 1. Toujours d'abord : contrôle de version, navigateur, cookies, en-têtes.
        parent::before($headless, $getBrowser);

        // 2. Notre seed, seulement quand la suite ouvre vraiment un navigateur.
        if ($getBrowser) {
            $this->setModuleTitle(self::SEEDED_TITLE);
        }
    }

    private function setModuleTitle(string $title): void
    {
        // Nom du conteneur PrestaShop : « ps » dans le workflow de l'article sur la CI.
        $container = $_ENV['PS_CONTAINER'] ?? getenv('PS_CONTAINER') ?: 'ps';

        // prestashop:config n'existe qu'à partir de PrestaShop 8.
        $command = sprintf(
            'docker exec %s php /var/www/html/bin/console prestashop:config set PSFLOWDEMO_TITLE --value %s 2>&1',
            escapeshellarg($container),
            escapeshellarg($title)
        );

        exec($command, $output, $status);
        if ($status !== 0) {
            throw new \RuntimeException("Seed impossible :\n" . implode("\n", $output));
        }
    }

    public function init()
    {
        $this->importPage('Modules\Psflowdemo\Home', domain: 'Tests');

        extract($this->pages);

        $this
        ->describe('Titre semé avant la suite')
        ->it('affiche le titre semé sur la home', function () use ($modulesPsflowdemoHomePage) {
            $modulesPsflowdemoHomePage->goToPage('home');

            Expect::that($modulesPsflowdemoHomePage->getBlockTitle())
                ->isTheSameAs(self::SEEDED_TITLE);
        })
        ->it('remet le titre par défaut', function () {
            $this->setModuleTitle('Bienvenue sur notre boutique');
        });
    }
}

Le même schéma vaut pour un code promo : remplacez la commande par un docker exec -i ps php qui crée un CartRule, comme au niveau 2.

Cleanup : ce que Flashlight change

L’annexe module tiers insistait sur le cleanup end-of-suite parce qu’elle décrivait une boutique de recette persistante — chaque run laisse des traces, il faut nettoyer derrière soi.

Dans un pipeline Flashlight, la logique s’inverse : chaque run démarre une boutique neuve et sa base (Flashlight n’embarque pas MySQL : la base tourne dans un conteneur à côté), et les deux sont jetés à la fin. Le cleanup est gratuit, il se fait tout seul, à une condition en local : détruire la base avec son volume (docker compose down -v), sinon elle survit au prochain démarrage.

Deux mondes :

  • Boutique de recette permanente (un serveur qui tourne, sur lequel plusieurs runs tapent) → cleanup obligatoire, fixtures side-effect sur la vraie base.
  • Flashlight en CI (conteneur jetable) → cleanup inutile, fixtures peuvent être aussi lourdes qu’on veut, on ne pollue rien.

Le choix de niveau de fixtures dépend beaucoup de ce facteur. Sur Flashlight, on peut se permettre le niveau 1 avec des données conséquentes. Sur boutique persistante, on préfère les niveaux 2 et 3 avec un cleanup soigné.

Mapping usages → niveau

Pour se repérer :

FixtureBon niveau
Module installé et configuré1 (init-scripts)
Devise, langue, transporteur, méthode de paiement1
Produits catalogue de base1
Compte client de test standard1
Jeu de N produits dont N varie entre runs2 (step CI)
Fixtures extraites d’un système externe (import quotidien)2
Code promo unique à un scénario3 (before())
Panier abandonné à un état précis3

Notes

Dans la Série PrestaFlow — article 17 sur 23