PrestaFlow : scénarios paramétrés, chaînés, data-driven
Trois mécaniques qu’on n’avait qu’effleurées
L’article d’introduction a montré l’utilisation basique d’un Scenario fourni par la lib :
$this
->describe('Parcours d\'achat invité')
->scenario(GuestCheckout::class);
Ce qu’on n’avait pas creusé, ce sont les trois mécaniques qui rendent ce pattern réutilisable et paramétrable :
- Les
$paramssur unScenario, qui permettent d’écrire ses propres scénarios réutilisables avec des valeurs par défaut surchargeables à l’appel. - Le
store()/retrieve()surTestsSuite, qui partage un état entreitet entre scénarios enchaînés dans la même suite. - Le
->with([...]), qui rejoue tous lesitde la suite contre plusieurs jeux de données. Data-driven testing sans effort.
Cette annexe couvre les trois, avec des cas d’usage concrets sur psflowdemo et les modules qu’on a croisés en chemin.
Écrire un Scenario paramétrable
Un Scenario PrestaFlow est une classe qui étend PrestaFlow\Library\Scenarios\Scenario et implémente steps($testSuite). Ce que vous ajoutez : une propriété $params avec vos valeurs par défaut.
Regardez le vrai code de GuestCheckout livré avec la lib :
class GuestCheckout extends Scenario
{
public $params = [
'locale' => 'fr',
'productUrl' => '1-1-hummingbird-printed-t-shirt.html',
'cartQuantity' => 1,
'guestEmail' => 'pf-guest@example.com',
'firstName' => 'PrestaFlow',
'lastName' => 'Guest',
// adresse, téléphone, etc.
];
public function steps($testSuite)
{
// ... utilise $this->getParam('productUrl'), etc.
}
}
À l’invocation, on peut passer un tableau qui écrase les défauts, sans avoir à toucher au Scenario :
$this
->describe('Achat invité anglais avec deux exemplaires')
->scenario(GuestCheckout::class, [
'locale' => 'en',
'cartQuantity' => 2,
'guestEmail' => 'qa+' . time() . '@example.test',
]);
Trois clefs surchargées, tout le reste reprend les défauts du Scenario. Aucun code dupliqué, l’intention métier tient en cinq lignes.
Un Scenario maison pour psflowdemo
Reprenons le fil rouge. Créons un Scenario réutilisable qui met à jour le titre du bloc, avec le titre en paramètre :
<?php
namespace Tests\Scenarios;
use PrestaFlow\Library\Expects\Expect;
use PrestaFlow\Library\Scenarios\Scenario;
class UpdatePsflowdemoTitle extends Scenario
{
public $params = [
'title' => 'Bienvenue sur notre boutique',
];
public function steps($testSuite)
{
$testSuite->importPage('BackOffice\Login');
$testSuite->importPage('Modules\Psflowdemo\Configuration', domain: 'Tests');
$testSuite->importPage('Modules\Psflowdemo\Home', domain: 'Tests');
extract($testSuite->pages);
$testSuite
->it('login BO', function () use ($backOfficeLoginPage) {
$backOfficeLoginPage->goToPage('login');
$backOfficeLoginPage->login();
})
->it('met à jour le titre', function () use ($modulesPsflowdemoConfigurationPage) {
$modulesPsflowdemoConfigurationPage->openConfiguration($this->getGlobals()['BO']['URL']);
$modulesPsflowdemoConfigurationPage->fillTitle($this->getParam('title'));
$modulesPsflowdemoConfigurationPage->save();
})
->it('vérifie le rendu sur la home', function () use ($modulesPsflowdemoHomePage) {
$modulesPsflowdemoHomePage->goToPage('home');
Expect::that($modulesPsflowdemoHomePage->getBlockTitle())
->contains($this->getParam('title'));
});
return $testSuite;
}
}
À partir de ce Scenario, une seule ligne dans une suite suffit pour tester n’importe quel titre :
$this->describe('Titre standard')
->scenario(UpdatePsflowdemoTitle::class, ['title' => 'Bienvenue chez nous']);
Enchaîner plusieurs Scenario dans une suite
Une suite peut chaîner plusieurs ->scenario(). C’est utile pour composer un test complexe à partir de briques :
public function init()
{
$this
->describe('Setup + parcours + cleanup')
->scenario(SeedBaseCatalog::class)
->scenario(UpdatePsflowdemoTitle::class, ['title' => 'Promotion Halloween'])
->scenario(GuestCheckout::class, [
'productUrl' => 'produit-halloween.html',
])
->scenario(CleanupTestOrders::class);
}
Chaque ->scenario() ajoute ses it à la suite au moment de l’appel ; ils s’exécutent ensuite dans l’ordre, et partagent l’état de la page Chrome — un login BO fait dans le premier scenario reste actif pour le second, à condition qu’aucun scénario intermédiaire ne redémarre le navigateur. Revers de la médaille : par défaut, dès qu’un it échoue, tous les suivants de la suite passent en skipped, y compris ceux des scénarios suivants (->skipWhenFailed(false) désactive ce comportement).
Pour partager de la donnée entre scénarios (l’id d’une commande créée dans le premier, à réutiliser dans le troisième), on utilise store() / retrieve() sur la suite.
store() / retrieve() : partager l’état
Le pattern, tel qu’on le retrouve dans un projet en production :
->it('navigue vers la commande de test', function () use ($ordersPage) {
$ordersPage->goToOrder($this->getParam('orderId'));
$this->store('productQuantity', $ordersPage->getQuantity());
// ...
})
->it('vérifie qu\'un split a bien changé la quantité', function () use ($ordersPage) {
Expect::that((int) $ordersPage->getQuantity())
->isNotTheSameAs((int) $this->retrieve('productQuantity'));
});
Trois choses à retenir :
store()etretrieve()vivent surTestsSuite(pas surPage). Depuis unit, on les appelle via$this(le contexte duitest la suite).- La donnée persiste tant que la suite tourne, entre
itcomme entre scénarios chaînés. - Un
retrieve()d’une clef inexistante retournenull(ou la valeur$defaultsi passée). Utile pour unitoptionnel qui vérifie si la clef a été posée avant.
Cas d’usage typique : un scenario CreateOrder qui stocke l’orderId, puis un scenario RefundOrder qui le retrouve pour cibler la bonne commande.
Data-driven : ->with([...])
Voici la mécanique qui n’avait été mentionnée nulle part dans la série. La lib expose une méthode ->with() qui accepte un tableau de datasets — chaque it de la suite est exécuté une fois par dataset :
$this
->describe('Titre du bloc — jeux variés')
->with([
['title' => 'Court'],
['title' => 'Un titre de longueur moyenne pour tester le wrap'],
['title' => rtrim(str_repeat('Très long ', 20))],
['title' => 'Accents : éàü'],
['title' => '&<>"\''],
])
->it('accepte et affiche', function () use ($modulesPsflowdemoHomePage) {
// Le dataset courant est accessible via getParam().
$expected = $this->getParam('title');
// ...on met à jour la config avec ce titre, puis vérifie sur la home.
Expect::that($modulesPsflowdemoHomePage->getBlockTitle())->contains($expected);
});
Un seul it écrit, cinq exécutions à chaque run. Attention, la sortie CLI ne les distingue pas : elle affiche cinq fois le même titre de it. Pour savoir quel jeu a cassé, lisez le message d’assertion (il contient la valeur attendue) ou le results.json, où chaque exécution porte le numéro du jeu (dataset, à partir de 1) et ses valeurs (datasets).
Utile pour : validation d’entrées limites (vide, très long, unicode, caractères réservés), matrice de combinaisons (locale × type de client × transporteur), tests de valeurs de bord d’un prix ou d’une quantité.
Ce que ces trois mécaniques changent en pratique
Prises ensemble, elles vous permettent d’atteindre un pattern qui manquait à l’arsenal :
- Écrire un scénario métier une seule fois (paramètres +
stepsgénériques) - L’invoquer avec dix jeux de données (
->with([...])) ou chaîné à d’autres (->scenario(...)) - Partager l’état entre les étapes (
store/retrieve)
En termes de lignes de test : un projet qui utilise ces trois mécaniques a nettement moins de code de test que le même projet écrit avec des it inline dupliqués.
Interaction avec les autres annexes
- Fixtures —
store()est le compagnon naturel des fixtures niveau 3 (overridebefore()) : lebefore()crée une donnée, en stocke la clef viastore(), lesitla réutilisent viaretrieve(). - Debug — un
itqui itère sur 20 datasets et tombe au 12e : la sortie CLI ne montre pas le dataset actif, mais le message d’assertion donne la valeur en cause, etresults.jsonindique le numéro du jeu. Un$this->log()dans leitaffiche aussi les valeurs voulues sous lePASS/FAIL. Leskip()peut cibler leitentier pendant le debug, l’->with()continue de fournir les datasets. - Multi-locales — un dataset ne change pas la locale : les Pages sont importées (et leurs URLs résolues) dans
init(), avant que le moindre dataset ne soit lu. Une cleflocaledans un dataset n’est qu’une valeur comme une autre pourgetParam(), et elle perd face au'locale'd’un Scenario. La locale se fixe par le paramètre'locale'passé au Scenario, ou pour toute la suite parPRESTAFLOW_LOCALE. Pour couvrir plusieurs locales, écrivez une suite par locale ou relancez la même suite avec une autre valeur dePRESTAFLOW_LOCALE, comme dans l’annexe multi-locales.
Notes
Dans la Série PrestaFlow — article 20 sur 23