PrestaFlow : composer ses scénarios avec ->scenario()
Décor
Une suite PrestaFlow décrit un flow métier : loguer, ajouter au panier, checkout, retrouver la commande en BO, changer son statut. En pratique on retape la même séquence de préambule dans dix suites : login, panier, checkout — puis on assert la seule chose qui varie. C’est du boilerplate.
La bibliothèque expose depuis la 1.0.0 le concept de scénario réutilisable : une classe qui empile des it() et qu’on colle dans une suite avec ->scenario(::class). Deux scénarios se chaînent sur la même suite, et, depuis la 1.3.0, l’état passe de l’un à l’autre via ->store() / ->retrieve(). Les scénarios prêts à l’emploi CheckoutOrder et ManageOrder sont arrivés avec la 1.5.0. Le boilerplate disparaît.
La primitive : ->scenario()
Signature côté suite :
public function scenario($class, array $params = [])
{
// …
}
Trois choses qui se passent quand on l’appelle :
- Une instance de
$classest créée avec$this(la suite courante) +$params. - Le constructeur du scénario fusionne les params avec ses défauts, règle sa propre locale et sa version (celles du scénario, pas celles de la suite), puis appelle
steps($testSuite). La suite garde au passage les params fusionnés, quegetParam()lira pendant l’exécution. - Dans
steps(), chaque$testSuite->it(...)s’ajoute à la suite courante — le scénario ne crée pas une suite parallèle, il alimente celle qu’on lui passe.
scenario() renvoie la suite : l’appel est chaînable, donc plusieurs ->scenario() s’empilent dans le même flow.
Écrire un scénario réutilisable
Un scénario est une classe qui étend PrestaFlow\Library\Scenarios\Scenario et redéfinit steps($testSuite). Convention : quelques params par défaut, tout it() va sur $testSuite, l’état à partager va dans $this->store(), appelé depuis les closures it().
Exemple minimal : un scénario qui crée un client de test en BO et retient son email et son mot de passe pour la suite. La lib ne fournit pas de méthode de création de client (sa Page BackOffice\Customer ne définit que deux sélecteurs) : on s’appuie sur une Page du projet, tests/prestaflow/Pages/v9/BackOffice/CustomerForm/Page.php, qui l’étend.
<?php
namespace Tests\Pages\v9\BackOffice\CustomerForm;
use PrestaFlow\Library\Pages\v9\BackOffice\Customer\Page as BasePage;
class Page extends BasePage
{
public function defineSelectors()
{
return [
// emailInput et saveButton viennent de la Page de la lib
...parent::defineSelectors(),
'firstNameInput' => 'input[name="customer[first_name]"]',
'lastNameInput' => 'input[name="customer[last_name]"]',
'passwordInput' => 'input[name="customer[password]"]',
];
}
/**
* À appeler sur le formulaire « Ajouter un client » du BO.
*/
public function create(array $customer): void
{
$this->waitVisible($this->getSelector('firstNameInput'));
$this->setValue($this->getSelector('firstNameInput'), $customer['firstname']);
$this->setValue($this->getSelector('lastNameInput'), $customer['lastname']);
$this->setValue($this->getSelector('emailInput'), $customer['email']);
$this->setValue($this->getSelector('passwordInput'), $customer['password']);
$this->clickAndWaitReload($this->getSelector('saveButton'));
}
}
Le scénario, dans tests/prestaflow/Scenarios/CreateTestCustomer.php (le namespace Tests\Scenarios est résolu par le même autoload PSR-4 Tests\\ → tests/prestaflow/ que vos Pages) :
<?php
namespace Tests\Scenarios;
use PrestaFlow\Library\Scenarios\Scenario;
class CreateTestCustomer extends Scenario
{
public $params = [
'firstname' => 'Jean',
'lastname' => 'Test',
'password' => 'PrestaFlow2026!',
];
public function steps($testSuite)
{
$testSuite->importPage('BackOffice\Login');
$testSuite->importPage('BackOffice\Customers');
$testSuite->importPage('BackOffice\CustomerForm', domain: 'Tests');
extract($testSuite->pages);
$email = 'test-' . uniqid() . '@example.test';
$testSuite
->it('log en BO', function () use ($backOfficeLoginPage) {
$backOfficeLoginPage->goToPage('index');
$backOfficeLoginPage->login();
})
->it('crée le client', function () use ($backOfficeCustomersPage, $backOfficeCustomerFormPage, $email) {
$backOfficeCustomersPage->goTo();
$backOfficeCustomersPage->clickAndWaitReload($backOfficeCustomersPage->getSelector('newCustomerButton'));
$backOfficeCustomerFormPage->create([
'firstname' => $this->getParam('firstname'),
'lastname' => $this->getParam('lastname'),
'email' => $email,
'password' => $this->getParam('password'),
]);
// $this ici = TestsSuite (closures rebindées à l'exécution),
// donc store() / retrieve() sont accessibles pour la suite.
$this->store('CreateTestCustomer.email', $email);
$this->store('CreateTestCustomer.password', $this->getParam('password'));
});
return $testSuite;
}
}
Trois idées à retenir :
$this->getParam('name')dans les closuresit()lit le param passé au compose, ou la valeur par défaut de$params. Dans le corps desteps(),$thisest le scénario, qui n’a pas degetParam(): on y lit$this->params['name'].$this->store('key', $val)dans les closuresit()(contexte suite, rebindé par.call($this)côté runner) empile de l’état accessible plus tard.- Namespacing des clés
storepar nom de scénario (CreateTestCustomer.email) — évite les collisions quand deux scénarios composés stockent chacun leuremailou leurpassword.
Composer deux scénarios avec passage d’état
Le scénario suivant reprend l’état posé par le premier. Il n’a rien à savoir de l’implémentation du premier — juste connaître les clés qu’il expose.
<?php
namespace Tests\Scenarios;
use PrestaFlow\Library\Scenarios\Scenario;
class LoginAsTestCustomer extends Scenario
{
public function steps($testSuite)
{
$testSuite->importPage('FrontOffice\Login');
extract($testSuite->pages);
$testSuite
->it('log en FO avec le client créé', function () use ($frontOfficeLoginPage) {
// Le mot de passe vient aussi du store : il suit le param passé à CreateTestCustomer.
$email = $this->retrieve('CreateTestCustomer.email');
$password = $this->retrieve('CreateTestCustomer.password');
$frontOfficeLoginPage->goToPage('login');
$frontOfficeLoginPage->login($email, $password);
});
return $testSuite;
}
}
Et la suite qui les compose fait tenir la logique en quelques lignes utiles :
<?php
namespace Tests\Suites;
use PrestaFlow\Library\Expects\Expect;
use PrestaFlow\Library\Tests\TestsSuite;
use Tests\Scenarios\CreateTestCustomer;
use Tests\Scenarios\LoginAsTestCustomer;
class NewCustomerJourney extends TestsSuite
{
public function init()
{
$this->importPage('FrontOffice\Login');
extract($this->pages);
$this
->describe('Nouveau client — création BO puis login FO')
->scenario(CreateTestCustomer::class)
->scenario(LoginAsTestCustomer::class)
->it('est connecté à son compte', function () use ($frontOfficeLoginPage) {
// Le lien de déconnexion n'apparaît qu'une fois connecté
Expect::that()->customerIsLogged($frontOfficeLoginPage->getSelector('logoutLink'));
});
}
}
Deux ->scenario() chaînés, une assertion propre à ce test, et c’est tout. Le corps de NewCustomerJourney reste centré sur ce qu’il vérifie ; la mécanique création + login est réutilisée telle quelle.
Passer des params au compose
Le second argument de ->scenario() remplace les défauts déclarés dans $params du scénario. Utile pour piloter le même scénario dans plusieurs suites avec des variations.
$this
->scenario(CreateTestCustomer::class, [
'firstname' => 'Alice',
'lastname' => 'Belge',
'password' => 'AutreMdp2026!',
])
->scenario(LoginAsTestCustomer::class);
Grâce au store, LoginAsTestCustomer se connecte avec AutreMdp2026! sans rien savoir de ce param. Dans ses closures it(), le scénario lit ses params via $this->getParam('firstname') plutôt que par une variable capturée : getParam() se rabat sur le dataset courant quand le param n’est pas défini, ce qui garde le scénario compatible avec les datasets (cf. scénarios paramétrés & chaînage).
Le cas d’école livré avec la lib : OrderLifecycle
C’est exactement le pattern derrière la suite OrderLifecycle fournie par la lib (namespace PrestaFlow\Library\Tests\Suites\Scenarios) :
class OrderLifecycle extends TestsSuite
{
public function init()
{
$this
->describe('Create an order then manage its lifecycle in the BackOffice')
->scenario(\PrestaFlow\Library\Scenarios\CheckoutOrder::class)
->scenario(\PrestaFlow\Library\Scenarios\ManageOrder::class);
}
}
CheckoutOrderfait le tunnel FO → confirmation → contrôle en BO, et stocke l’orderReferencedès la page de confirmation.ManageOrderretrouve cette référence, la filtre en BO, change le statut, ajoute une note, pose un tracking.
Zéro glue entre les deux, sinon le contrat de clé orderReference que les deux honorent. C’est la vraie promesse du pattern : découpler la mécanique du flow métier de la variation qu’on veut tester.
Interaction avec les autres annexes
- Écrire son premier scénario — l’article d’introduction crée UNE Page et UN scénario dans une suite. Cette annexe montre comment sortir le scénario de la suite pour le réutiliser.
- Scénarios paramétrés & chaînage — les params passés au compose et les datasets
->with([...])sont deux dimensions complémentaires : compose = variations qualitatives (nature du flow), datasets = variations quantitatives (X couples de valeurs sur le même flow). - Fixtures & seed data — un scénario réutilisable qui crée de l’état (client, produit, adresse) n’est pas une fixture : c’est une suite exécutable qui pose de l’état. Les deux se complètent — fixture pour l’état permanent de la boutique, scénario pour l’état spécifique d’un flow.
- Bibliothèque de Pages partagée — au-delà d’une équipe/projet, on partage aussi ses scénarios. Le pattern est identique aux Pages : un composer package, un namespace,
->scenario(External\CheckoutOrder::class).
Notes
Dans la Série PrestaFlow — article 36 sur 36