Archives par mot-clé : behat

[Tutoriel] Comment piloter Chrome avec Behat ?

Source https://www.pxfuel.com/en/free-photo-oclmc

Suite au succès de mon précédent tutoriel sur Behat, ce tutoriel est le premier d’une série consacrée au pilotage des navigateurs.

J’utilise le projet todo list disponible sur GitHub pour ce tutoriel.

Choix du navigateur

Avant de commencer, il est nécessaire de choisir le navigateur à utiliser pour l’exécution des tests. Pour ce premier tutoriel, nous allons utiliser Chromium sous Linux.

Il est possible de piloter un grand nombre de navigateurs pour ordinateur ou smartphone. Ce sera l’objet de prochains tutoriels.

Lancement du navigateur

Les navigateurs basés sur Chromium peuvent être pilotés directement par Behat grâce à l’extension Mink et le pilote Chrome dmore/chrome-mink-driver.

Avant de lancer Behat, il est nécessaire de lancer le navigateur avec les bonnes options pour que Behat puisse l’utiliser.

Chromium via snap

Commandes pour installer Snapd et Chrmium : apt-get install snapd
snap install core chromium

Le binaire à utiliser dans la commande ci dessous est : /snap/bin/chromium

Exécuter cette commande dans un terminal pour utiliser Chromium installé sur le système. Il faudra le laisser ouvert pendant toute la durée des tests.

chromium --enable-automation --disable-background-networking --no-default-browser-check --no-first-run --disable-popup-blocking --disable-default-apps --disable-translate --disable-extensions --no-sandbox --enable-features=Metal --remote-debugging-port=9222 https://127.0.0.1 

Pour mettre fin à l’exécution du navigateur, appuyer sur Ctrl + C.

Pour exécuter Chromium sans la fenêtre, ajouter l’option --headless.

Configurer le projet

Ce tutoriel part du principe que vous avez déjà installé Behat tel que présenté dans le précédent tutoriel. Si ce n’est pas le cas, réaliser l’installation avant de continuer.

Ajouter l’extension Behat et le driver Mink au projet:

composer require --dev dmore/behat-chrome-extension

Dans le fichier behat.yml.dist, ajouter la configuration suivante:

default:
    extensions:
        DMoreChromeExtensionBehatServiceContainerChromeExtension: ~
        BehatMinkExtension:
            base_url: "https://127.0.0.1:8000/"
            javascript_session: chrome_headless
            sessions:
                chrome_headless:
                    chrome:
                        api_url: "http://127.0.0.1:9222"
                        validate_certificate: false

Configurer les scénarii

Pour exécuter des scénarii avec un navigateur piloté, il est nécessaire d’ajouter le tag @javascript au début du fichier de fonctionnalité ou d’un scénario en particulier.

Exécuter les tests

Le navigateur est déjà lancé, il est nécessaire de lancer le serveur web. Pour cela j’utilise Rymfony (vidéo de présentation). Pour le lancer, exécuter APP_ENV=test rymfony serve -d depuis le dossier racine du projet.

Le projet utilise une base de données SQLite. Si votre projet a besoin d’un serveur MariaDb ou Postgres, lancez-le pour que votre projet ait accès aux données.

Voici le résultat en vidéo

Pour vous montrer le résultat obtenu, voici la vidéo de la réalisation de ce tutoriel sur le projet Todo list

Voir la vidéo : https://www.youtube.com/watch?v=VKZXb5yFYMA

Cas pratique d’écriture d’un test automatique en français

Source: https://www.pxfuel.com/en/free-photo-jepee

Dans mon précédent article sur les tests automatisés, je parlais d’écrire les tests en français. Je vous ai présenté un test et parlé de son anatomie. Vous connaissez maintenant la base de la syntaxe Gherkin.

Des outils comme Cucumber et Behat permettent d’interpréter les fichiers de fonctionnalité (feature) contenant les scénarii de test.

Terminologie

Avant d’aller plus loin, un petit point terminologie, nous réalisons ici des tests d’acceptation. Il est courant d’entendre parler de tests fonctionnels mais ce terme peut s’appliquer à d’autres types de test.

Voyons maintenant comment écrire le scénario pour tester la connexion d’un utilisateur sur notre application web.

Déterminer les cas d’usage de la fonctionnalité à tester

Nous allons donc tester la connexion d’un utilisateur. Quelles sont les différentes situations qu’un utilisateur peut rencontrer lors de la connexion ?

Le premier cas est une connexion réussie avec son identifiant et son mot de passe.

Le deuxième cas est une connexion échouée en raison d’un mauvais mot de passe.

Le troisième cas est une connexion échouée en raison d’un identifiant inconnu.

Et c’est tout. Notre application n’utilise pas de double authentification et ne dispose pas de la mémorisation de l’utilisateur (remember me).

Déterminer les pré-requis, l’état initial du test

Maintenant que nous avons tous les cas de figure métier de la connexion d’un utilisateur, quel doit être l’état du système avant de commencer un cas de test ?

Le premier pré-requis est que nous devons ouvrir un navigateur Internet et charger la page de connexion d’un utilisateur sur notre application web.

Le second est qu’un utilisateur doit être présent en base de données pour permettre la réalisation de deux des scénarii.

Et c’est tout.

Déterminer ce qui sera vérifié

Maintenant, qu’allons-nous vérifier ?

Dans le cas d’une connexion réussie, nous devons arriver sur la page du compte de l’utilisateur.

Dans le cas d’une connexion échouée, nous devons être sur la page de connexion et le message « Identifiant ou mot de passe invalide ».

Pour des raisons de sécurité, la distinction entre un compte inconnu et un mot de passe erroné n’est pas affichée.

Écrire les scénarii

Maintenant, nous pouvons passer à l’écriture d’un fichier de fonctionnalité (feature).

#language fr
Fonctionnalité: Connexion d'un utilisateur

  Contexte:
    Etant donné que l'utilisateur "test" est enregistré avec le mot de passe "test123456!"
    Et que je suis sur la page de connexion

  Scénario: Connexion réussie
    Lorsque je me connecte en tant que "test" avec le mot de passe "test123456!"
    Et que je valide la connexion de l'utilisateur
    Alors je dois être sur la page du compte de l'utilisateur "test"

  Plan du Scénario: Identifiant incorrect
    Lorsque je me connecte en tant que "<identifiant>" avec le mot de passe "<mot_de_passe>"
    Alors je dois être sur la page de connexion
    Et le message d'erreur "Identifiant ou mot de passe incorrect" est visible

  Exemples:
    | identifiant | mot_de_passe |
    | test        | abracadabra  |
    | toto        | abracadabra  |

Avez-vous remarqué que j’ai regroupé les deux scénarii d’échec ? C’est une fonctionnalité de la syntaxe Gherkin. Il est possible de jouer plusieurs fois le même scénario avec des informations différentes.

Explication du fichier

Mais revenons au début du fichier. Si vous comparez avec ce que je vous ai montré dans mon précédent article, il y a beaucoup de chose en plus !

Contexte

A la ligne 1, nous trouvons la définition de la langue utilisée dans le fichier.

A la ligne 2, le terme « Fonctionnalité: » est un mot clé Gherkin qui lui indique le début du test d’une fonctionnalité. Le texte qui suit le mot clé est un commentaire (il peut être sur plusieurs lignes).

A la ligne 4, le terme « Contexte » indique que les phrases (ou étapes) qui suivent (ligne 5 et 6) doivent être réalisées avant chaque scénario de la fonctionnalité. Le contexte est très utile pour alléger les scénarii. Cela permet de disposer le système dans un état donné commun à toute la fonctionnalité.

Scénario simple

Le premier scénario débute à la ligne 6 et ressemble plus à ce dont j’ai parlé dans le précédent article.

Cependant, il y deux points que je souhaite vous montrer :

Les textes entre guillemets est celui qui sera saisi sur la page de connexion par l’outil de tests. Il est possible de récupérer ainsi des informations depuis les phrases permettant leur réutilisation.

A la ligne 10 (comme à la ligne 6 et 16), la phrase commence par le mot clé « Et que » (ou « Et »). Ce mot clé est utilisé en remplacement des autres mots clés pour éviter la répétition et obtenir un texte compréhensible.

Plan de scénario

A la ligne 13 nous découvrons le mot clé « Plan du Scénario ». Celui-ci indique à l’outil qu’il doit exécuter le scénario plusieurs fois en fonction d’un tableau de données.

Dans les phrases du plan, les éléments qui changent entre chaque exécution sont placés entre les signes « inférieur » et « supérieur ». Le texte présent entre ces deux signes désigne le nom de la colonne où trouver la donnée.

Les données en elles-mêmes sont présentées sou forme de tableau après le mot clé « Exemples » (ligne 18 et suivantes).

Récapitulons

Dans cet article, nous avons vu comment analyser une fonctionnalité et écrire un fichier de scénario contenant tous les cas que nous souhaitons tester.

Nous avons également découvert quelques mots clés supplémentaires nous permettant d’écrire des scénarii simples et concis.

La simplicité des phrases et la concision des tests permet à tous les intervenants de comprendre facilement et rapidement le fonctionnement de l’application.

Cadeau

Après avoir bien travaillé, je vous offre un cadeau. En plus d’avoir écrit un test qui sera exécuté avant chaque livraison de votre application, nous venons d’écrire de la documentation.

Oui, les tests d’acceptation (comme les autres d’ailleurs) font partie de la documentation de votre application.

La documentation fournie par les tests a la particularité d’être maintenue à jour. Il serait dommage de s’en priver.

Intéressé par le sujet ?

Merci d’avoir lu jusqu’à la fin ! Contactez-moi pour en discuter et restez connecté !

[Tutoriel] Test Behat sur un projet Symfony

https://www.pxfuel.com/en/free-photo-xzywf
https://www.pxfuel.com/en/free-photo-xzywf

But de ce tutoriel

Après vous avoir présenté le design pattern PageObject dans un précédent billet de blog, nous allons voir ensemble comment le mettre en pratique sur un projet Web.

Ici nous vérifions que toutes les briques de l’application fonctionnent ensemble correctement. Nous allons utiliser le driver Symfony de Mink pour permettre la simulation des requêtes HTTP et la récupération des réponses.

J’ai utilisé ce projet pour écrire ce billet de blog.

Installation

Nous allons avoir besoin de plusieurs dépendances uniquement pour le développement.

La première est Behat lui-même. Ensuite Doctrine data fixtures permettra de réinitialiser la base de données entre chaque scenarii.
Puis viennent les extensions pour Behat telles que MinkExtension et SymfonyExtension. Mink BrowserKit Driver est requise par MinkExtension sans être requise dans son composer.json.

$ composer require --dev behat/behat doctrine/data-fixtures friends-of-behat/mink-browserkit-driver friends-of-behat/mink-extension friends-of-behat/page-object-extension friends-of-behat/symfony-extension

Si vous êtes sur la branche master du projet, Flex vous demandera la confirmation de l’application de la recette du packet friends-of-behat/symfony-extension. Il est préférable d’accepter.

Config

Le fichier behat.yml.dist a été ajouté à la racine du projet.

Modifier son contenu pour qu’il ressemble à celui-ci :

default:
    suites:
        default:
            contexts:
                - App\Behat\DemoContext

    extensions:
        Behat\MinkExtension:
            base_url: "https://localhost/"
            default_session: symfony
            sessions:
                symfony:
                    symfony: ~
        FriendsOfBehat\SymfonyExtension: null

Ici nous activons et configurons les extensions installées précédemment.

Pour permettre la suite des opérations, nous avons besoin de modifier le fichier config/services_test.yaml.

Ce fichier contient la configuration des services spécifiques à l’environnement de test.

Nous y définissons des alias, dont celui du générateur de code de double authentification pour le permettre d’être prévisible.

services:
    _defaults:
        autowire: true
        autoconfigure: true

    Doctrine\Persistence\ObjectManager: '@doctrine.orm.default_entity_manager'

    App\Security\CodeGeneratorInterface: '@App\Tests\Service\DefinedCodeGenerator'

    App\Tests\Service\:
        resource: '../tests/Service/*'

    App\Tests\Functional\:
        resource: '../tests/Functional/*'
        exclude: '../tests/Functional/{Page}'

    App\Tests\Functional\Page\:
        resource: '../tests/Functional/Page/*'
        parent: 'FriendsOfBehat\PageObjectExtension\Page\SymfonyPage'
        autoconfigure: false
        public: false
        autowire: true

    FriendsOfBehat\PageObjectExtension\Page\Page:
        abstract: true
        autoconfigure: false
        public: false
        autowire: true
        arguments:
            - '@behat.mink.default_session'
            - '@behat.mink.parameters'

    FriendsOfBehat\PageObjectExtension\Page\SymfonyPage:
        abstract: true
        autoconfigure: false
        public: false
        autowire: true
        parent: 'FriendsOfBehat\PageObjectExtension\Page\Page'
        arguments:
            - '@router'

Comme vous pouvez le voir, deux services abstraits sont définis pour les PageObject. Le service SymfonyPage sera utilisé par la configuration de tous les services présents dans le dossier tests/Functional/Page.

Notre projet est configuré.

Initialiser l’arborescence

Comme expliqué dans mon premier blog post’ sur le sujet, nous allons ajouter les dossiers qui contiendront les contextes et les PageObject.

tests
├── bootstrap.php
├── Functional
│   ├── Context
│   │   ├── Task
│   │   └── TodoList
│   ├── Hook
│   ├── Page
│   │   ├── Task
│   │   └── TodoList
│   └── Setup
└── Service

Dans le cas de mon projet, j’ai besoin de modifier le comportement du générateur de code de double authentification pour le rendre prévisible. C’est pour cela que le dossier Service a été ajouté.

Implémenter le service de génération de code

Dans le cas des tests, l’utilisation du hasard n’est pas possible. C’est pourquoi nous allons réimplémenter une interface pour définir un service spécifique au fonctionnement des tests.

Ajouter le fichier tests/Service/DefinedCodeGenerator.php avec son contenu :

<?php

declare(strict_types=1);

namespace App\Tests\Service;

use App\Security\CodeGeneratorInterface;

class DefinedCodeGenerator implements CodeGeneratorInterface
{

    public function generate(): string
    {
        return '1234';
    }
}

Ajouter les scenarii de test

Comme les tests utilisent le langage naturel, il est préférable de commencer par l’écriture des tests.

Ajouter le fichier features/Login.feature avec le contenu suivant :

#language: fr
Fonctionnalité: Connexion à l'application

  Contexte:
    Etant donné que l'utilisateur "todo@me.fr" est enregistré avec le mot de passe "MonSuperPassWord"

  Scénario: Connexion
    Etant donné que je suis sur la page de connexion
    Lorsque je me connecte en tant que "todo@me.fr" avec le mot de passe "MonSuperPassWord"
    Alors je dois être sur la page de double authentification
    Lorsque je saisis le code de double authentification
    Alors je dois être sur la liste des todo listes

Supprimer le fichier demo.feature.

Aucune des phrases utilisées dans le scénario n’est connue de Behat.

Contexte d’initialisation

La section Contexte: de la fonctionnalité permet la réalisation d’action avant chaque scénario. Cela permet d’éviter les répétitions en début de scénario.

La seule phrase présente permet d’ajouter un utilisateur et son mot de passe dans la base de données.

Comme il s’agit de mise en place des données utiles aux tests, nous sommes donc dans un contexte Setup et, comme cela concerne un utilisateur, nous allons donc ajouter le UserContext.

Ajouter le fichier tests/Functional/Setup/UserContext.php avec son contenu :

<?php declare(strict_types=1);

namespace App\Tests\Functional\Setup;

use App\Entity\User;
use Behat\Behat\Context\Context;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;

class UserContext implements Context
{
    /**
     * @var ObjectManager
     */
    private $objectManager;
    /**
     * @var EncoderFactoryInterface
     */
    private $encoderFactory;

    public function __construct(ObjectManager $objectManager, EncoderFactoryInterface $encoderFactory)
    {
        $this->objectManager = $objectManager;
        $this->encoderFactory = $encoderFactory;
    }

    /**
     * @Given /^l'utilisateur "([^"]+)" est enregsitré avec le mot de passe "([^"]+)"$/
     */
    public function registerUser($email, $password)
    {
        $user = new User();
        $user->setEmail($email);
        $user->setPassword($this->encoderFactory->getEncoder($user)->encodePassword($password, null));
        $this->objectManager->persist($user);
        $this->objectManager->flush();
    }

}

La phrase est maintenant définie grâce à une expression rationnelle ayant des parenthèses capturant là où nous souhaitons récupérer des informations.

Pour permettre l’utilisation de cette phrase dans les scénarii de test, il est nécessaire d’ajouter le namespace de cette classe dans la configuration de Behat :

default:
    suites:
        default:
            contexts:
                - App\Tests\Functional\Setup\UserContext

Maintenant, l’utilisateur sera ajouté avant l’exécution du scénario.

Contexte d’action

Pour les autres actions, nous allons définir un contexte par page. Cela permet de garder les choses rangées.

Les contextes seront enrichis au fur et à mesure que les tests seront écrits.

Action sur la page de connexion

Dans ce contexte, deux phrases seront définies : celle ouvrant la page de connexion et celle réalisant la connexion d’un utilisateur.

Voici le contenu de ce contexte :

// Fichier : tests/Functional/Context/LoginContext.php
<?php declare(strict_types=1);

namespace App\Tests\Functional\Context;

use App\Tests\Functional\Page\LoginPage;
use Behat\Behat\Context\Context;

class LoginContext implements Context
{
    /**
     * @var LoginPage
     */
    private $loginPage;

    public function __construct(LoginPage $loginPage)
    {
        $this->loginPage = $loginPage;
    }

    /**
     * @Given je suis sur la page de connexion
     */
    public function jeSuisSurLaPageDeConnexion()
    {
        $this->loginPage->open();
    }

    /**
     * @When /^je me connecte en tant que "([^"]+)" avec le mot de passe "([^"]+)"$/
     */
    public function connexion($email, $password)
    {
        $this->loginPage->login($email, $password);
    }
}

Vous voyez ainsi que le contexte ne contient pas beaucoup de code. Il ressemble à un adaptateur.

Voici le service PageObjet qui est implémenté dans la classe LoginPage :

// Fichier : tests/Functional/Page/LoginPage.php
<?php declare(strict_types=1);

namespace App\Tests\Functional\Page;

use FriendsOfBehat\PageObjectExtension\Page\SymfonyPage;

class LoginPage extends SymfonyPage
{
    public function getRouteName(): string
    {
        return 'app_login';
    }

    public function login($user, $password)
    {
        $this->open();
        $this->getDocument()->fillField('email', $user);
        $this->getDocument()->fillField('password', $password);
        $this->getDocument()->pressButton('Sign in');
    }
}

Ici également, il n’y a pas beaucoup de code. La fonction open est gérée par la classe SymfonyPage.

La méthode login ouvre la page de login, renseigne le formulaire de connexion puis le valide.

Pour activer ce contexte, ajoutons-le à la configuration de Behat.

# fichier : behat.yml.dist
default:
    suites:
        default:
            contexts:
                - App\Tests\Functional\Context\LoginContext

Action sur la page du second facteur

Maintenant que nous avons réalisé la prise en charge de la page de connexion, il est maintenant nécessaire de gérer la page de la double authentification.

Comme il s’agit d’une nouvelle page, nous allons donc ajouter un nouveau contexte et un nouveau service PageObjet étendant SymfonyPage.

// Fichier : tests/Functional/Context/TwoFactorContext.php
<?php declare(strict_types=1);

namespace App\Tests\Functional\Context;

use App\Tests\Functional\Page\TwoFactorPage;
use Behat\Behat\Context\Context;

class TwoFactorContext implements Context
{
    /**
     * @var TwoFactorPage
     */
    private $twoFactorPage;

    public function __construct(TwoFactorPage $twoFactorPage)
    {
        $this->twoFactorPage = $twoFactorPage;
    }

    /**
     * @When je saisis le code de double authentification
     */
    public function saisieDoubleAuth()
    {
        $this->twoFactorPage->sendCode('1234');
    }

    /**
     * @Then je dois être sur la page de double authentification
     */
    public function verifPageTwoFactor()
    {
        $this->twoFactorPage->verify();
    }
}
// Fichier : tests/Functional/Page/TwoFactorPage.php
<?php declare(strict_types=1);

namespace App\Tests\Functional\Page;

use FriendsOfBehat\PageObjectExtension\Page\SymfonyPage;

class TwoFactorPage extends SymfonyPage
{
    public function getRouteName(): string
    {
        return 'app_two_factor';
    }

    public function sendCode($code)
    {
        $this->verify();

        $this->getDocument()->fillField('password', $code);
        $this->getDocument()->pressButton('Send code');
    }
}

Également, ce contexte doit être ajouté dans la configuration de Behat pour être utilisable :

# fichier : behat.yml.dist
default:
    suites:
        default:
            contexts:
                - App\Tests\Functional\Context\LoginContext
                - App\Tests\Functional\Context\TwoFactorContext

Action sur la liste des ToDo listes

Maintenant, il reste une phrase non définie dans le scénario. Elle sera définie dans le contexte lié aux listes de ToDo Liste.

Comme pour les deux contextes précédents, nous en ajouterons un troisième IndexContext avec son PageObject IndexPage. Pour ne pas les confondre avec les autres, ils sont placés dans un dossier dédié aux fonctionnalités liées aux ToDo Listes.

// Fichier : tests/Functional/Context/TodoList/IndexContext.php
<?php declare(strict_types=1);

namespace App\Tests\Functional\Context\TodoList;

use App\Tests\Functional\Page\TodoList\IndexPage;
use Behat\Behat\Context\Context;

class IndexContext implements Context
{
    /**
     * @var IndexPage
     */
    private $indexPage;

    public function __construct(IndexPage $indexPage)
    {
        $this->indexPage = $indexPage;
    }

    /**
     * @Then je dois être sur la liste des todo listes
     */
    public function jeDoisEtreSurLaListeDesTodoListes()
    {
        $this->indexPage->verify();
    }
}

Même si l’objet page contient peu de code, il est là pour disposer des fonctions prédéfinies par la classe SymfonyPage.

// Fichier : tests/Functional/Page/TodoList/IndexPage.php
<?php declare(strict_types=1);

namespace App\Tests\Functional\Page\TodoList;

use FriendsOfBehat\PageObjectExtension\Page\SymfonyPage;

class IndexPage extends SymfonyPage
{
    public function getRouteName(): string
    {
        return 'todo_list_index';
    }
}

Également, ce contexte doit être ajouté dans la configuration de Behat pour être utilisable :

# fichier : behat.yml.dist
default:
    suites:
        default:
            contexts:
                - App\Tests\Functional\Context\LoginContext
                - App\Tests\Functional\Context\TwoFactorContext
                - App\Tests\Functional\Context\TodoListIndexContext

Contexte avant chaque scénario

Nous n’avons pour l’instant qu’un seul test. Le besoin de réinitialiser la base de données ne se fera sentir que lors de la prochaine exécution.

En effet, l’utilisateur existant déjà en base de données, une erreur SQL sera émise lors de la tentative d’ajout de l’utilisateur en base.

Pour éviter cela, il est possible d’exécuter du code automatiquement avant chaque scénario.

Ajouter le fichier tests/Functional/Hook/DoctrineContext.php avec son contenu :

<?php

declare(strict_types=1);

namespace App\Tests\Functional\Hook;

use Behat\Behat\Context\Context;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Doctrine\ORM\EntityManagerInterface;

class DoctrineContext implements Context
{
    /** @var EntityManagerInterface */
    private $entityManager;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    /**
     * @BeforeScenario
     */
    public function purgeDatabase()
    {
        $this->entityManager->getConnection()->getConfiguration()->setSQLLogger(null);
        $purger = new ORMPurger($this->entityManager);
        $purger->purge();
        $this->entityManager->clear();
    }
}

Pour l’activer, il est nécessaire d’ajouter ce contexte aux autres dans la configuration de Behat behat.yml.dist.

default:
    suites:
        default:
            contexts:
                - App\Tests\Functional\Hook\DoctrineContext

L’annotation @BeforeScenario indique à Behat qu’il faut exécuter cette fonction avant tous les scénarii.

Conclusion

Nous avons ajouté des contextes, configuré les services utiles pour les tests, configuré Behat, écrit le scénario de test et les définitions de chaque phrase. Il est maintenant possible d’exécuter les tests et de poursuivre l’écriture des scénarii.

Cette petite mise en pratique du design pattern PageObjet ne montre malheureusement pas tout son potentiel. En transposant ce procédé sur vos projets et vos besoins métier, vous pouvez entrevoir le potentiel d’une telle solution.

Je n’exclus pas la réalisation d’un ou plusieurs autres billets de blog sur le sujet pour montrer le potentiel de ce design pattern.

Dites-moi dans les commentaires si vous avez déjà utilisé ce pattern sur vos projets. Je suis à votre service si vous avez des questions.

Mise à jour du 26 janvier 2021: Les antislash des noms de classe ont été remis.

Behat et SymfonyPage

https://www.pxfuel.com/en/free-photo-jacvj
https://www.pxfuel.com/en/free-photo-jacvj

Behat et Mink

Behat est un outil de test fonctionnel dont les scénarii sont écrits en langage commun grâce à Gerkin.
Le concept est simple, le test est divisé en 3 parties « Étant donné que » pour donner les conditions préalables, « Lorsque » pour indiquer les actions réalisées pour le test, puis « Alors » pour vérifier que les actions réalisées ont produit le résultat souhaité.

Chaque ligne du test est une phrase complète qui commence par un mot-clé. Dans cette phrase, des éléments peuvent servir de données utiles pour le test.

Chaque phrase est associée à une méthode PHP grâce aux classes de contexte. La phrase est située dans une annotation au-dessus de la fonction à appeler. L’annotation peut contenir une expression rationnelle ou une phrase avec des placeholders commençant par deux points. Chaque parenthèse de capture de l’expression rationnelle ou placeholder sera un argument de la méthode PHP appelée pour exécuter la phrase du test.

Ex :

/**
 * @Then je dois voir le texte :texte
 */
public function jeDoisVoirLeTexte($texte)
{
}

C’est ainsi que les tests peuvent être exécutés.

Afin de ne pas se répéter, les contextes sont réutilisables.

Pour tester des pages Web HTML, l’utilisation de Mink est nécessaire. Il dispose de pilote (driver) pour différentes situations.

En voici quelques-uns, chacun avec ses avantages et inconvénients :

  • Sans serveur Web et Navigateur (extension Symfony) : Simule un objet Request et récupère l’objet Response.
  • Avec un serveur Web, mais sans Navigateur (extension Goutte) : Réalise de vraies requêtes HTTP via un client HTTP sur un serveur Web et obtient une réponse HTTP. Seul le JavaScript n’est pas exécuté.
  • Avec un serveur Web, mais avec un Navigateur (extension Selenium) : Pilote un navigateur Web sans fenêtre pour réaliser les actions sur le site Web et vérifier le résultat. Ces tests sont particulièrement longs à exécuter.

Pour éviter l’écriture de fonctions complexes dans les contextes, Mink abstrait les drivers. Le cas des tests avec JavaScript est particulier, car le pilote est impatient et il génère de faux échecs de test. L’ajout de délais dans les fonctions du contexte est nécessaire.

C’est ici que les choses se compliquent. Eviter l’écriture de contexte est possible grâce au contexte proposé par Mink. Cependant, vos tests ne ressembleront pas à de vrais tests fonctionnels, car ils contiendront des classes CSS, des xPath dans vos phrases.

Un autre point est qu’une phrase ne peut être déclarée qu’une seule fois et chaque contexte est indépendant des autres. Cela bloque l’héritage des contextes par défaut de Mink pour un seul contexte. Votre contexte gonflera pour contenir tous les cas d’actions sur les pages.

Concept de PageObject

Comment diviser cet énorme contexte en contextes plus petits et surtout mieux organisés ? Comment permettre l’écriture d’un contexte par page de l’application ou par concept ?

La première approche serait d’utiliser le RawMinkContext qui apporte le strict minimum des dépendances nécessaires pour écrire des cas de test et d’actions.
Cependant, mutualiser du code utile pour plusieurs phrases ne sera pas possible. Pour cela, l’utilisation des services externes au contexte est nécessaire.

Le pattern PageObject est venu résoudre ce problème. Nous pouvons en profiter dans Behat soit par l’extension SensioLab soit par l’extension du groupe Friend of Behat.

Mais qu’est-ce donc ?

Ce sont des services qui ont une dépendance vers Mink et qui permettront de réaliser des actions sur la page actuellement chargée. Un service PageObject permet de regrouper toutes les actions possibles d’une page ou d’un élément commun du site (le concept d’Element est plus adapté dans ce cas).

Le service PageObject expose les actions disponibles et les données à récupérer sur la page pour laquelle il est dédié. Il ne fait pas d’assertion (ou très très peu).

Ce service est ensuite injecté dans le service de contexte afin de pouvoir interagir avec la page.

L’extension proposée par Friends Of Behat propose un concept de SymfonyPage qui utilise le routeur Symfony pour appeler la page (donc, générer l’URL) et vérifier que le service PageObject va réaliser une action sur la bonne page.

C’est donc dans ce service PageObject que nous allons manipuler les éléments techniques de la page Web.

Conséquence dans les Context

La première conséquence est la possibilité de séparer les définitions des phrases en plusieurs contextes indépendants.

La seconde conséquence est la possibilité d’organiser les contextes en fonction des modules de l’application et sortir ceux relatifs à l’infrastructure telle que les hook Behat et les contextes liés à la préparation de la base de données pour le test.

Ainsi, voici une architecture possible pour le dossier de tests fonctionnels :

tests
└── Functional
    ├── Context
    │   ├── Admin
    │   │   ├── LoginContext.php
    │   │   ├── NotificationContext.php
    │   │   ├── Module1
    │   │   │   └── Module1Context.php
    │   │   └── Module2
    │   │       ├── IndexModule2Context.php
    │   │       └── ShowModule2Context.php
    │   └── Module2
    │       ├── DashboardContext.php
    │       └── LoginContext.php
    ├── Hook
    │   └── FixtureContext.php
    ├── Page
    │   ├── Admin
    │   │   ├── Account
    │   │   │   └── LoginPage.php
    │   │   ├── Module1
    │   │   │   └── IndexPage.php
    │   │   └── Module2
    │   │       └── ShowPage.php
    │   └── Module2
    ├── Setup
    │   └── MonEntiteContext.php
    └── Transform
        └── QuantityContext.php

Certains auront peut-être trouvé la source d’inspiration de cette organisation. C’est celle utilisée dans les tests du projet de e-commerce Sylius.

Enfin, une conséquence non négligeable est la nécessité d’écrire plus de fichiers PHP et de code pour réaliser les tests. Mais cette conséquence disparaît vite avec la mise en pratique.

Bénéfices

Les utilisateurs réguliers des tests fonctionnels avec Behat et Mink seront sûrement septiques. Mais n’avez-vous jamais eu besoin de dupliquer du code pour vérifier qu’une notification est bien présente et contient le texte souhaité ?

Cette façon de faire répond exactement à ce cas. Vous pouvez définir un service PageObject (ou plutôt Element dans ce cas) qui se concentrera sur la vérification des différentes notifications possibles. Vous l’injecterez dans tous les contextes qui en ont besoin dans l’exécution d’une phrase. Cela évitera des phrases comme « je dois voir la notification… » qui ne sont pas forcément utiles.

Un autre bénéfice des PageObject est de pouvoir injecter le service dans plusieurs contextes et ainsi éviter la duplication de code.

à vous de jouer !

Qu’en pensez-vous ? Comment réalisez-vous vos tests fonctionnels ?