Home Outils Retour d’XP sur le framework de test Codeception

Retour d’XP sur le framework de test Codeception

  André, Architecte technique PHP 10 min 28 octobre 2019

Dans cet article je vous propose un retour d’expérience quant à l’utilisation du framework de test Codeception, que j’ai eu l’occasion de mettre en place pour de nos clients dans le cadre d’une mission au sein de son équipe.

Contexte

Lors de mon intégration au sein cette équipe de développement (PHP uniquement), un des défis fut de migrer un site en Symfony 2 en Symfony 3 pour des raisons évidentes de sécurité et de maintenance. Mais cette montée de version fut également le moment idéal pour refondre une partie de la logique métier ainsi que le système de paiement de cette plateforme.

Une refonte est souvent une occasion parfaire pour améliorer la qualité et la robustesse du code et mettre en place des tests unitaires et fonctionnels. C’est ce que nous avons décidé de faire …

Présentation

Codeception fût le framework idéal correspondant parfaitement à notre besoin. Au-delà du fait que ce soit du PHP, il permet de faire des tests d’acceptances, fonctionnels et unitaires et donc de couvrir tous les aspects d’une application ou d’un site web.

Les tests d’acceptances 

En temps normal, ils peuvent être effectués par une personne non technique. Cette personne peut être un testeur, un responsable ou même un client, et n’a besoin que d’un navigateur Web pour vérifier le bon fonctionnement de votre site.

Codeception permet, de manière simple, de reproduire ces actions à travers des scénarios et de les exécuter automatiquement.

Deux solutions sont proposées par le framework pour mettre en place ces tests d’acceptances :

  • PHP Browser

Solution simple et rapide car il n’est pas nécessaire d’exécuter un navigateur. C’est un crawler utilisant Guzzle et Symfony BrowserKit qui permet d’interagir avec les pages HTML.

En revanche, cette solution a des inconvénients, et pas des moindre, car les interactions sur une page web sont limitées aux simples clics sur des liens ou boutons de formulaires. Les champs d’un formulaire, quant à eux, ne peuvent être renseignés, ce qui peut être problématique pour valider une fonctionnalité.

  • WebDriver

C’est un protocole permettant de piloter automatiquement les navigateurs (comme Chrome ou Firefox) via Selenium (recommandé par l’équipe de Codeception), PhantomJS ou ChromeDriver.

Codeception utilise la bibliothèque facebook/php-webdriver de Facebook comme implémentation PHP du protocole WebDriver.

Ces solutions peuvent être complémentaires et permettent de tester votre site web ou application.

Plus d’infos ici sur les tests d’acceptances

Les tests unitaires

Codeception se base sur PHPUnit pour les tests unitaires, donc si vous êtes un adepte de tests PHPUnit, vous êtes en terrain conquis. Le framework va simplement ajouter une couche d’abstraction qui va simplifier certaines tâches récurrentes.

Plus d’informations ici sur les tests unitaires.

Les tests fonctionnels

Les tests fonctionnels sont pratiquement identiques aux tests d’acceptances, ils sont écrits de la même manière, à une différence majeure : ceux-ci ne nécessitent pas de serveur Web. Concrètement, il suffit de définir les variables $_REQUEST, $_GET et $_POST avant d’exécuter un test.

Pour ce faire, Codeception permet de se connecter à différents frameworks PHP supportant les tests fonctionnels : Symfony, Laravel5, Yii2, Zend Framework et autres. Il faut simplement activer le module souhaité dans la configuration pour pouvoir l’utiliser.

Plus d’informations ici sur les tests fonctionnels.

On va pouvoir également pousser les tests, en se connectant à un SGBD. Codeception propose des modules pour se connecter à différentes bases de données et on va ainsi pouvoir compter le nombre d’éléments en base (après une insertion par exemple), ou vérifier qu’une donnée à bien été mise à jour.

Plus d’informations ici sur la gestion des données.

Les tests d’acceptances en pratique

Les exemples qui vont suivre sont spécifique à l’architecture mise en place mais peuvent servir comme référence pour votre cas d’utilisation.

Architecture de l’infrastructure :

Pour ma part, l’architecture était composée de : – 2 bases de données (Oracle, MariaDB) – 1 API (Symfony) – Plusieurs applications Web (Symfony, WordPress, PHP natif)

Les applications Web doivent passer par l’API pour consommer les données en base :

CodeCeptionPour éviter d’installer et de configurer Codeception sur l’ensemble des applications web, nous avons pris la décision de l’isoler sur un seul et unique serveur afin de centraliser l’automatisation des tests sur l’ensemble des plateformes.

Nous avons également une interaction directe avec nos différentes bases de données (pour s’assurer de la cohérence des valeurs modifiées sur celles-ci), ainsi qu’avec notre API (nous verrons en détail pourquoi).

condeception_2

Architecture du framework :

Nous avons donc autant de répertoire que d’applications pour bien faire la distinction entre les différents tests, même si certains vont beaucoup se ressembler.

Exemple : L’authentification sur les différentes applications

Toutes les applications s’interfacent avec l’API pour la phase d’authentification. Certaines règles de gestion sont communes à l’ensemble des applications et sont appliquées directement au niveau de l’API.

Mais ce n’est pas toujours le cas, prenons le cas d’un compte utilisateur qui se trouve en situation d’impayé, nous allons lui bloquer l’accès à certaines applications, et selon l’application, surcharger le message d’erreur.

Nous pourrons ainsi nous assurer que nous avons bien le comportement attendu sur chacune de nos applications à l’aide de tests d’acceptances.

Configuration :

Analysons en détails le fichier de configuration « /Api/test/acceptance.suite.yml » :

actor: AcceptanceTester
modules:
    enabled:
        - WebDriver
        - REST

    config:
        # https://codeception.com/docs/modules/WebDriver
        WebDriver:
            url: 'http://my-app.com'
            browser: chrome
            host: 'localhost' ## il faut mettre l'IP de la VM où tourne Selenium

            port: 4444
            window_size: 2048x1536
            capabilities:
                chromeOptions:
                    args: ["--headless", "--disable-gpu", "--disable-dev-shm-usage", '--no-sandbox']

        MultiDb:
            timezone: "+00:00"
            connections:
                Oracle:
                    dsn: 'oci:dbname=//db-local:1521/db_name;charset=utf8'
                    user: 'user'
                    password: 'password'
                    reconnect: true
                Mysql:
                    dsn: "mysql:host=db-local;port=3306;dbname=db_name;charset=utf8"
                    user: 'user'
                    password: 'password'
                    reconnect: true

        REST:
            depends: PhpBrowser

Nous avons 2 modules activés : « WebDriver » et « REST ».

Concernant la configuration de ces modules : – WebDriver : il faudra renseigner les informations sur le protocole utilisé (Selenium, PhantomJS ou ChromeDriver), et l’url d’accès à notre application – REST : il faut spécifier si on doit se baser sur un navigateur ou un Framework

Développement d’un test via Webservices

Maintenant que nous avons terminé de configurer notre projet, nous allons développer deux tests de webservices. Le premier sera l’authentification d’un utilisateur avec des données erronées et le second avec des données valides.

Classe « /Api/tests/acceptance/LoginTest.php »

<?php

class LoginTest
{
    /<strong>
     <em> Authentification avec un login/mot de passe incorrect
     </em>
     <em> @param AcceptanceTester $I
     </em>/
    public function tryLoginError(AcceptanceTester $I)
    {
        $I->haveHttpHeader('Content-Type', 'application/json');
        $I->sendPOST('/login_check', '{"username":"good_login", "password":"wrong_password"}');

        // On vérifie la réponse obtenue
        $I->seeResponseIsJson();
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::UNAUTHORIZED);

        // On vérifie que le format de réponse obtenu est bien celui qui est attendu
        $I->seeResponseMatchesJsonType(
            [
                'error' => [
                    'id' => 'string',
                    'code' => 'string',
                    'message' => 'string'
                ]
            ]
        );

        $I->seeResponseContainsJson('Le couple e-mail/mot de passe est incorrect.');
    }

    /</strong>
     <em> Authentification avec un login/mot de passe correct
     </em>
     <em> @param AcceptanceTester $I
     </em>/
    public function tryLoginSuccess(AcceptanceTester $I)
    {
        $I->haveHttpHeader('Content-Type', 'application/json');
        $I->sendPOST('/login_check', '{"username":"good_login", "password":"good_password"}');

        // On vérifie la réponse obtenue
        $I->seeResponseIsJson();
        $I->seeResponseCodeIs(\Codeception\Util\HttpCode::OK);

        // On vérifie que le format de réponse obtenu est bien celui qui est attendu
        $I->seeResponseMatchesJsonType(
            [
                'token' => 'string',
                'refresh_token' => 'string',
                'refresh_token_ttl' => 'integer'
            ]
        );
    }
}

Si nous décortiquons un peu plus en détails ces différentes méthodes, vous pourrez constater que nous avons effectué des appels en POST avec des en-têtes spécifiques et que nous avons pu vérifier non seulement le code HTTP et le format de retour mais également la structure de la réponse.

Développement d’un test via une interface web

<?php

class LoginTest
{
    /<strong>
     <em> @param AcceptanceTester $I
     </em>/
    public function tryLoginError(AcceptanceTester $I)
    {
        // On va sur la page d'authentification
        $I->amOnPage('/login');

        // On s'assure de trouver le texte suivant sur la page
        $I->see('Identification');

        // On soumet le formulaire
        $I->submitForm('.login_form', ['_username' => '', '_password' => '']);

        // On s'assure de trouver le selecteur suivant 1 fois (celui qui contient le message d'erreur)
        $I->seeNumberOfElements('//*[@id="contentLayer"]/div/div[1]/ul/li', 2);

        // On s'assure de trouver le texte suivant sur la page
        $I->see('Cette valeur est obligatoire.');

        // On soumet le formulaire
        $I->submitForm('.login_form', ['_username' => 'good_user', '_password' => 'wrong_password']);

        // On s'assure de trouver le selecteur suivant 1 fois (celui qui contient le message d'erreur)
        $I->seeNumberOfElements('//*[@id="contentLayer"]/div/div[1]/ul/li', 1);

        // On s'assure de trouver le texte suivant sur la page
        $I->see('Le couple email/mot de passe est incorrect.');
    }



    /</strong>
     * @param AcceptanceTester $I
     */
    public function tryLoginSuccess(AcceptancelTester $I)
    {
        // On va sur la page d'authentification
        $I->amOnPage('/login');

        // On s'assure de trouver le texte suivant sur la page
        $I->see('Identification');

        // On soumet le formulaire
        $I->submitForm('.login_form', ['_username' => 'good_user', '_password' => 'good_password']);

        // On s'assure de trouver le texte suivant sur la page
        $I->see('Bonjour good_user !');
    }
}

Ces tests seront effectués sur l’url que vous aurez spécifié dans la configuration de WebDriver.

Dans notre premier test, nous validons le formulaire de login sans renseigner de valeurs, puis nous renseignons avec des valeurs erronées, afin de s’assurer que le comportement obtenu est bien celui attendu.

Pour ce qui est du second test, nous renseignons cette fois-ci un identifiant correct et vérifions que l’utilisateur est bien redirigé vers une autre page.

Je vous partage (seulement) 2 méthodes mais sachez que nous en avons mis en place 13 tests différents sur l’authentification (compte bloqué, compte expiré, compte sans permissions, etc…) Libre à vous de déterminer si oui ou non le test est pertinent.

Lancement des tests

Maintenant que nous avons développé quelques tests, nous allons voir ensemble comment les jouer. Il faut savoir que vous avez la possibilité de jouer l’ensemble des tests d’une classe :

cd /var/www/project

php codecept.phar run tests/acceptance/LoginTest.php

Mais vous avez également la possibilité de ne jouer qu’un seul test. Pour ce faire il suffit de préciser le nom de cette méthode lors du l’exécution des tests :

cd /var/www/project

php codecept.phar run tests/acceptance/LoginTest.php:tryLoginSuccess

Lorsque vos tests sont en succès vous aurez un retour console du type :

Acceptance Tests (2) --------------------------------------------------------------------------------------------

✔ LoginTest: Try login error (0.50s)

✔ LoginTest: Try login success (0.53s)

--------------------------------------------------------------------------------------------

En revanche si vous avez des erreurs, pour avez la possibilité d’ajouter l’option « -vvv » lors de l’exécution de la commande afin d’avoir un maximum de détails sur l’erreur. Le framework génère également une capture d’écran du navigateur lorsqu’une erreur est levée, ce qui vient compléter les erreurs de la console.

Aller plus loin dans les tests

Selon les sites, et les fonctionnalités à tester, nous n’avons pas besoin des mêmes données en base.

En effet, lorsque nous testons le workflow d’inscription, le fait que notre base de données soit peuplée ou non nous importe peu, contrairement au workflow d’authentification où là, la présence de données est obligatoire.

Nous avons pris le parti d’écrire des fixtures pour l’ensemble des tests d’acceptances. Chaque test possède ses propres fixtures associées. Celles-ci sont toutes présentes sur notre API puisque c’est la seule application qui est en mesure se connecter aux différentes bases de données.

Nous avons donc une méthode supplémentaire dans notre classe de test qui va exécuter ces fixtures sur notre serveur API distant (via une connexion SSH). Afin de pouvoir exécuter des commandes bash sur le serveur, il faut activer un module supplémentaire : Cli

Nous pouvons aller encore plus loin dans les tests car après avoir joué les fixtures, nous allons pouvoir vérifier que les données ont bien été insérées en base de données.

Pour ce faire il faudra également activer un autre module : « MutiDb » qui n’est pas natif au framework. Nous avons utilisé la librairie « iamdevice/codeception-multidb ».

Donc voilà à quoi ressemble notre fichier de configuration finale :

actor: AcceptanceTester
modules:
    enabled:
        - WebDriver        - Cli
        - MultiDb
        - REST

    config:
        # https://codeception.com/docs/modules/WebDriver
        WebDriver:
            url: 'http://my-app.com'
            browser: chrome
            host: 'localhost' ## il faut mettre son l'IP de la VM où tourne Selenium


            port: 4444
            window_size: 2048x1536
            capabilities:
                chromeOptions:
                    args: ["--headless", "--disable-gpu", "--disable-dev-shm-usage", '--no-sandbox']


        MultiDb:
            timezone: "+00:00"
            connections:
                Oracle:
                    dsn: 'oci:dbname=//db-local:1521/db_name;charset=utf8'
                    user: 'user'
                    password: 'password'
                    reconnect: true
                Mysql:
                    dsn: "mysql:host=db-local;port=3306;dbname=db_name;charset=utf8"
                    user: 'user'
                    password: 'password'
                    reconnect: true


        REST:
            depends: PhpBrowser

Nous allons pouvoir s’assurer de la cohérence des données présentes en base grâce à des méthodes comme :

// Authentification à la base de données Oracle
$I->amConnectedToDatabase('Oracle');
// On s'assure que la table "users" ne contient qu'une seule ligne
$I->seeNumRecords(1, 'users');

Vous pourrez retrouvez l’ensemble des méthodes ici

Conclusion

La mise en place des tests d’acceptances a permis d’une part la montée en compétence de l’ensemble de l’équipe sur les fonctionnalités du métier, mais a surtout permis de jouer des tests de non-régression avant chaque livraison et nous avons ainsi augmenté drastiquement la robustesse et la pérennité de notre application.

En revanche la maintenance des tests a un coût et à chaque évolution sur un écran, il faut repasser sur le test ou les tests qui sont impactés.

Il est donc préférable de modifier 1 fois 5 champs de formulaire que 5 fois 1 seul champ… car le coût en temps n’est pas du tout comparable !

Mon retour d’expérience sur le sujet s’arrête là, je vous invite à lire la documentation officielle pour en apprendre plus sur le framework et les possibilités qu’il peut apporter à votre application.

Lire les articles similaires

Laisser un commentaire

Social Share Buttons and Icons powered by Ultimatelysocial