Intégration Continue & Tests : un exemple avec GitlabCI et Codeception

Dans ce tutotrompe, nous allons voir ensemble comment avoir un processus de développement robuste qui permettra de vérifier le bon fonctionnement de ton code avant le déploiement. On verra ensuite un cas simple d’utilisation avec Codeception et des tests sur la partie front d'un site.

L’intégration continue ?

L’intégration continue, pour résumer, est une suite d’étapes qui permet de vérifier le bon fonctionnement de ton application. Ici, on vise l'amélioration de la qualité du code, et donc, du produit final. Cela va nous permettre de détecter les anomalies le plus tôt possible et donc de les corriger avant de les déployer sur un serveur de recette ou de production.

Le premier prérequis pour la mettre en place, c’est que ton code soit être versionné. D’ailleurs, ça devrait être un prérequis sur n’importe quel projet sur lequel tu travailles, qui plus est, en équipe. Mais c’est un autre débat, intéressons-nous au fonctionnement.

Dans les grandes lignes, l’idée est que, dès qu’un bout de code est publié, on puisse tester celui-ci et vérifier qu’il n’y a aucun effet de bord et aucune régression. Cette étape d’intégration continue s’insère donc entre l’étape de développement et l’étape de déploiement du code en production. C’est elle qui va conditionner le déroulement des opérations : si c’est validé, super, on pourra fusionner le code dans la branche souhaitée, sinon, on regarde ce qui ne va pas, on corrige, on itère jusqu’à ce que tous les voyants soient au vert. Et après ça, on se détend.

Opérationnellement parlant...

C’est un joli concept mais comment met-on ça en place ?

L’étape la plus importante ce sont des tests, des tests et des tests. On va parler ici de mettre en place des tests unitaires et des tests d’acceptance.

Pour ceux du fond, je rappelle que les tests unitaires vont être des tests à vision microscopiques au fonctionnement bête et méchant : On teste une fonction, on varie les paramètres envoyés à la fonction et on vérifie le résultat. Si j’ai une fonction qui calcule une distance entre 2 coordonnées, je vais vérifier plusieurs jeux de données et vérifier le résultat de ces fonctions. Et attention à bien vérifier également les paramètres incorrects. On veut savoir si cela marche dans de bonnes conditions, et savoir si on lève les alertes correctement quand les données sont invalides !

Et ce, idéalement pour la majeure partie de ton code.

Les tests d’acceptance quant à eux concernent ton application en entier. On va assembler tous les modules et vérifier que l’expérience utilisateur est conforme à ce que l’on souhaite de bout-en-bout. Et là où on se concentrait sur l’analyse du code dans les tests unitaires, ici, on fonctionne en mode bête et méchant : Je ne sais pas comment c’est fait, je veux juste que tout marche !

Suivant ton projet ...

Attention, si tu ne le savais pas, les tests, ça prend du temps. Beaucoup de temps. Parfois, on peut même passer plus de temps à développer les tests d’une fonctionnalité que la fonctionnalité en elle-même. Tu vas me dire, oui mais je n’ai pas beaucoup de temps, je dois livrer pour hier … Mais il faut voir cela comme un investissement. Tu pourras concentrer tes efforts sur les nouvelles fonctionnalités et avec moins d'anomalies, le client sera satisfait de ton travail, tu auras moins d’urgences à gérer et tu seras plus serein au quotidien.

Il y de nombreuses approches vis-à-vis des tests :

  • Par exemple, tu es dans un projet de type Agile, ce que tu vises n’est pas forcément une deadline charrette comme on dit dans le métier mais principalement la qualité : Adopte une approche TDD, soit Test Driven Développent. Dans ce cas-là, on lit les spécifications, on écrit les tests qui correspondent. Et seulement après, on développe afin que chaque test soit concluant. C’est très couteux en temps mais ce type d’approche offre souvent les meilleurs résultats en termes de qualité tout au long de la vie d’un projet.

  • Ton chef de projet détecte une anomalie après que tu aies livré une nouvelle version d’un module. Pas de problème, corrige la et profites-en pour écrire le test associé. Peut-être même qu’il y avait déjà des tests mais que tu n'avais pas pensé à cette configuration qui provoquait le bug !

  • Tu reprends un projet où il n’y a aucun test ? Identifie les processus les plus critiques de l’application pour prioriser l’écriture de tests. Dans le cadre d’un site E-commerce par exemple, vérifie que l’ajout d’un produit au panier fonctionne, puis que le processus de commande ne rencontre aucun problème !

L’idée est d’investir du temps pour que toute l'équipe impliquée dans le projet, toi, tes autres collègues, ton chef de projet et ton client, soyez le plus serein possible. Cela te permettra également de faire évoluer ton projet en vérifiant qu’il n’y a aucun effet de bord et aucune régression sur tout le périmètre du projet ! Essayez de grouper les modifications de fonctionnalités ! Chaque modification entraine également une modification des tests, alors autant grouper pour gagner un maximum de temps !

Les outils d’intégration continue

Il existe énormément d’outils d’intégration continue : tous ont leurs spécificités. Certains sont ou peuvent intégrés directement dans ton dépôt de code, d’autres outils vont devoir être hébergés soit dans le cloud soit sur un de tes serveurs et vont être utilisés dès que du code sera envoyé sur ton repo. La différence peut se faire sentir également au niveau du prix : certains seront open-source et/ou gratuits, d’autres avec une partie limitée gratuite et payante pour avoir accès à toutes les fonctionnalités.

Ce qui risque d’orienter ton choix est d’abord la solution que tu utilises pour versionner ton code. Il n’y a pas de solution miracle mais parmi les outils les plus connus, on peut citer pour avoir une vision hétérogène de la chose :

  • Jenkins, à installer sur un de tes serveurs, gratuit et extrêmement polyvalent,

  • GitLabCI, intégré directement dans ton repo sur GitLab, tout sera intégré dans un seul et même endroit ce qui peut être intéressant,

  • TravisCI, une solution en ligne qui s’interface très bien avec les projets GitHub et qui permet une mise en place facile

GitLabCI

Nous allons voir plus en détail le fonctionnement et la configuration de GitLab CI.

Pas de chichi, de la même manière que TravisCI, la conf se fait grâce à un fichier nommé .gitlab-ci.yml à la racine de ton dépôt.

Ici, tu vas pouvoir tout configurer à partir de ce fichier. On va te présenter un exemple de configuration mais épluchez la documentation et vous verrez alors toutes les possibilités de l’outil.

L’idée principale de GitlabCI est de configurer une “pipeline”. C’est à dire une succession d’étapes ou “steps” qui vont constituer ton processus de validation de code, de tests, et éventuellement pousser jusqu’au déploiement de manière automatique ou non.

Chaque étape sera constituée de plusieurs tâches ou “jobs” qui seront lancées les unes à la suite des autres.

Chaque tâche lancera un conteneur Docker, comme une sorte d’image virtuelle, qui lancera un script que tu configureras. Si le script marche, on passe à l’étape suivante, s’il échoue, on arrête tout.

Regardons ici en détail la pipeline :

La pipeline visible sur gitlab

Ici, nous avons 4 étapes :

  • Codeqa, qui s’occupera de vérifier la qualité du code (vérification des dépréciations et coding style),

  • Test, qui lancera les tests unitaires et d’acceptance,

  • Build, qui s’occupera de créer un package de l’application, pour un futur déploiement,

  • Et pour finir Deploy, qui contiendra les tâches pour déployer notre application, grâce à Ansible, sur des serveurs prédéfinis mais nous ne couvrirons pas cette partie. Peut-être dans une prochaine vidéo si cela t’intéresse, dans ce cas-là, faites-le nous savoir dans les commentaires !

Avec la configuration par défaut, chaque tâche qui échoue arrête l’exécution de la pipeline. Dans ce cas-là, on regarde l’erreur, on corrige et on recommit.

Voici le fichier .gitlab-ci.yaml de la pipeline ci-dessus.

image: jakzal/phpqa:php7.2-alpine

before_script:
    - composer install

stages:
    - codeqa
    - test
    - build
    - deploy

cache:
    paths:
        - vendor/

php-deprecation-detector:
    stage: codeqa
    script:
        - deprecation-detector check src vendor
    allow_failure: false
    only:
        - release


phpcs:
    stage: codeqa
    script:
        - phpcs -v --standard=PSR2 ./src --ignore=src/Migrations/*
    allow_failure: false
    only:
        - release


phpunit:
    stage: test
    script:
        - phpdbg -qrr ./bin/phpunit --coverage-text --colors=never
    allow_failure: false
    only:
        - release

codeception-api:
    stage: test
    services:
        - selenium/standalone-chrome:latest
    script:
        - php -S localhost:8000 --docroot public &>/dev/null&
        - ./vendor/bin/codecept run --steps --debug
    allow_failure: false
    artifacts:
        paths:
            - tests/codeception/_output/
    variables:
        SELENIUM_HOST: "selenium-standalone-chrome"


build:withvendor:
    stage: build
    artifacts:
        paths:
            - build/
    before_script:
        - composer install --optimize-autoloader
    script:
        - mkdir build/
        - cp -r bin config templates ansible public src vendor composer.* .env build/
    only:
        - release
        - develop

.deploy:
    script: &deploy
        - pip install ansible
        - ansible-galaxy install ansistrano.deploy ansistrano.rollback
        - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
        - eval $(ssh-agent -s)
        - mkdir -p ~/.ssh
        - ssh-add <(echo "$SSH_PRIVATE_KEY")
        - echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/private_key
        - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
        - apt-get update -y && apt-get install rsync -y
        - echo $VAULT_PASS > ~/.vaultpassword
        - ansible-playbook -i $HOST_FILE ansible/deploy.yml --vault-password-file=~/.vaultpassword --extra-vars "stage=$CI_ENVIRONMENT_NAME"

deploy:dev:
    stage: deploy
    image: python:3.6
    variables:
        VAULT_PASS: $VAULT_PASS_DEV
        HOST_FILE: 'ansible/env/hosts'
        INSTANCE_NUMBER: 'httpdocs'
    environment:
        name: dev
        url: https://url-dev/
    before_script:
        - ""
    script: *deploy
    only:
        - develop

deploy:test1:
    stage: deploy
    image: python:3.6
    variables:
        VAULT_PASS: $VAULT_PASS_TEST
        HOST_FILE: 'ansible/env/hosts'
        INSTANCE_NUMBER: 'sd/test1'
    environment:
        name: test1
        url: https://url-test-1/
    before_script:
        - ""
    script: *deploy
    when: manual
    only:
        - release

deploy:test2:
    stage: deploy
    image: python:3.6
    variables:
        VAULT_PASS: $VAULT_PASS_TEST
        HOST_FILE: 'ansible/env/hosts'
        INSTANCE_NUMBER: 'sd/test2'
    environment:
        name: test2
        url: https://url-test-2/
    before_script:
        - ""
    script: *deploy
    when: manual
    only:
        - release
        

Sans rentrer trop dans les détails, on va configurer à partir de quel image docker nous allons lancer nos scripts, (ligne “image”)

Puis, on définit une commande qui se lance avant chaque tâche : ici un composer install afin d’installer les dépendances de notre projet. (ligne “before_script”)

Ensuite, on définit nos étapes ou "stages”, (lignes “stages” et suivantes)

Après, on définit un répertoire de cache. Ce répertoire évitera d’installer les dépendances à chaque tâche afin de gagner du temps d’exécution. Il n’y a aucune raison que les dépendances évoluent entre chaque tâche dans notre cas. (ligne “cache”)

Pour finir, on va lister nos tâches, en leur attribuant un nom, si possible assez facile à lire pour s’y retrouver côté pipeline. (prendre pour exemple le bloc “codeception”)

On lie une tache à une étape, et on définit le script associé à cette étape. On peut lancer autant de commandes que l’on veut dans celui-ci. (ligne “script” et 2 suivantes du bloc “codeception”)

On peut définir ensuite une configuration précise pour exécuter une tache uniquement sur une certaine branche, ne l’exécuter que manuellement, autoriser éventuellement son échec, etc. (lignes “when”, “allow_failure”, “only” du bloc “deploy:test1”)

Regardons maintenant plus en détail la configuration et le fonctionnement de Codeception.

Codeception

Le framework de test Codeception permet de faire toutes sortes de tests : acceptance, unitaire, API, … Le langage utilisé est le PHP mais avec seulement quelques notions de code, même un non-développeur peut écrire des tests. L’idée est de décrire les actions à effectuer et les résultats attendus, le tout grâce à un code très verbeux et compréhensible.

-PhpStormWebDriver
Moteur de navigateurGuzzle + Symfony BrowserKitChrome ou Firefox
JavascriptNonOui
La méthode “see / seeElement” vérifie ...Que le texte soit présent dans la sourceQue le texte soit bien affiché pour l’utilisateur
Lit les en-têtes HTTPOuiNon
Prérequis systèmePHP avec l’extension CurlSelenium Standalone Server + Chrome ou Firefox
VitesseRapideLent

D'après le tableau du site https://codeception.com/docs/03-AcceptanceTests

Il y a deux moteurs principaux pour les tests et chacun a ses avantages et inconvénients :

  • Le premier, PhpBrowser, la solution la plus rapide car elle nécessite très peu de configuration. Toutes tes requêtes seront des requêtes HTTP sans navigateur. Un peu comme si tu faisais un appel Curl d’une page. Tu vas donc récupérer la source de cette page et vérifier les informations présentes. Par contre, les interactions utilisateurs ne seront pas testables. Cette solution est très pratique et très rapide pour tester les API par exemple.

  • Le second, WebDriver, la deuxième solution, pilote un vrai navigateur (Chrome ou Firefox suivant tes besoins). On va donc naviguer sur ton application ou sur ton site comme le ferait un vrai utilisateur. L’avantage est que tu vas pouvoir “cliquer” sur des boutons, exécuter du code javascript, remplir des formulaires à la volée, tester les messages d’erreur en cas d’oubli de champ, etc.
    Cette solution implique également d’installer et de lancer Selenium Server qui va permettre de piloter le navigateur. Par contre, les tests seront plus longs à exécuter qu’avec la solution PhpBrowser.

Pour installer Codeception, lance la commande composer suivante à la racine de ton projet :

    composer  require "codeception/codeception" --dev

Puis, lance la configuration Codeception :

    php  vendor/bin/codecept  bootstrap

Tests avec PhpBrowser

Crée ensuite notre premier fichier de test :

    php  vendor/bin/codecept  generate:cest acceptance AccueilYoutube

L’idée ici sera de vérifier que des informations soient bien présentes sur la page d’accueil de Youtube grâce au module PhpBrowser.

Configurons déjà Codeception en ouvrant le fichier tests/acceptance.suite.yml qui définira la configuration de tous nos tests d’acceptance.

    # Codeception Test Suite Configuration  
    #  
    # Suite for acceptance tests.  
    # Perform tests in browser using the WebDriver or PhpBrowser.  
    # If you  need  both  WebDriver and PHPBrowser tests - create a separate suite.  
      
    actor: AcceptanceTester  
    modules:  
      enabled:  
        - PhpBrowser:  
          url: https://www.youtube.fr/  
        - \Helper\Acceptance
      
      step_decorators: ~

On active ici le module PhpBrowser, on spécifie l’adresse du site à tester et on active le Helper d’Acceptance qui va contenir toutes les fonctions nécessaires aux tests pour vérifier la présence d’éléments, etc

Ouvre le fichier créé dans tests/acceptance/AccueilYoutubeCest.php et configurons ensemble notre premier test !

    <?php  
      
      class  AccueilYoutubeCest  
      {  
      public  function _before(AcceptanceTester $I)  
      {  
      }  
        
      // tests  
      public  function  tryToTest(AcceptanceTester $I)  
      {  
        $I->amOnPage('/');  
          
        $I->see('Accueil');  
        $I->see('Se connecter');  
        $I->seeInTitle('YouTube');  
      }  
    }

Ici, on va donc vérifier que sur la page d’accueil (URL "/"), nous voyons bien le texte “Accueil”, “Se connecter” et que le titre de la page contient bien “YouTube”.

Pour le lancer, utilise la commande :

    php  vendor/bin/codecept run --steps

Le résultat sur le terminal

On voit bien ici toutes les étapes, et que le résultat du test est concluant !

Tests avec SeleniumServer

En pré-requis, il faudra t'assurer que Java soit installé sur ta machine. Télécharge le fichier .jar de Selenium Server dans sa dernière version sur le site https://selenium.dev/downloads/, puis téléchargez le ChromeDriver qui pilotera ton navigateur chrome sur https://chromedriver.chromium.org/. Pour information, les étapes sont sensiblement identiques si tu souhaites le tester avec Firefox (https://github.com/mozilla/geckodriver/releases). Mets les deux fichiers dans le même répertoire et lance le serveur grâce à cette commande :

    java -jar -Dwebdriver.chrome.driver=chromedriver.exe selenium-server-standalone-3.141.59.jar

Maintenant configurons Codeception pour utiliser le module Webdriver. Nous allons donc modifier notre fichier acceptance.suite.yml comme ceci :

# Codeception Test Suite Configuration  
#  
# Suite for acceptance tests.  
# Perform tests in browser using the WebDriver or PhpBrowser.  
# If you  need  both  WebDriver and PHPBrowser tests - create a separate  suite.  
  
actor: AcceptanceTester  
  modules:  
    enabled:  
      - WebDriver:  
        url: https://www.youtube.fr/  
        browser: chrome  
      - \Helper\Acceptance  
    step_decorators: ~

Une fois la configuration modifiée, sauvegarde et relance ton test !

    php  vendor/bin/codecept run --steps

Tu vas voir la même sortie dans ta console mais tu auras remarqué chaque étape du test dans ton navigateur !

Codeception va également te générer des rapports d’erreur lorsque tout ne se déroule pas comme prévu. Ils seront disponibles dans différents formats et te montreront ce qui s’est passé face à ce qui était attendu. Des captures d’écran seront même disponibles lorsque tu utilises le WebDriver !

Je te laisserai regarder la documentation de Codeception pour des fonctionnalités plus poussées mais parmi celles-ci on peut citer :

  • Le module FileSystem qui permet de tester le système de fichier local, par exemple pour vérifier que des fichiers existent, qu’ils possèdent le bon contenu, etc

  • Le module FTP, qui permet de se connecter à un serveur et de vérifier également l’existence de répertoires ou de fichiers,

  • Le module Db qui va vérifier en base de données des informations, pratique pour vérifier que l’action que tu effectues via un test est bien enregistrée côté serveur,

  • Etc

Tests d’API

Pour les tests API, on pourra également vérifier plusieurs choses :

  • Les codes HTTP de réponses,

  • La structure des JSON ou XML renvoyés,

  • Le type de chaque propriété des objets,

  • La valeur de ces propriétés

Pour ceux-ci, il faudra utiliser la commande suivante pour générer un test :

    php  vendor/bin/codecept  generate:cest api  NomDuTest

Tu auras accès également à des méthodes qui te permettront de t'authentifier auprès de l’API lorsque tu auras récupéré tes jetons d’authentification, ou à des méthodes pour créer des requêtes GET ou POST par exemple.

A toi ensuite de bien écrire tes tests pour correspondre parfaitement à ton application et ta logique.

Pour aller plus loin...

Je ne peux que te conseiller de regarder la documentation de Codeception qui est remplie d’exemples plus avancés, par exemple, comment remplir un formulaire, comment cliquer sur certains éléments, etc.

Pour finir, j’aimerais préciser plusieurs points quant à l’utilisation de Codeception.

Premièrement, dans le projet dont la pipeline est illustrée au début de cette vidéo, nous lançons Codeception avec configuration Webdriver et donc Selenium Server pendant la même étape que les tests unitaires. Nous l’avons mis ici car nous ne testons que notre API et que nous avions besoin de récupérer un jeton d'authentification auprès d’un service qui nécessite une connexion via navigateur à ce moment-là.

Dans un cas classique, on devrait d’abord déployer notre code sur un serveur de développement pour ensuite tester lancer les tests API et fonctionnels de l’application.

A minima, je rappelle que tu devrais avoir 4 environnements pour un projet :

  • Un serveur local, pour tes développements,

  • Un serveur de développement, pour lancer les tests d’acceptance,

  • Un serveur de recette pour que tes clients / testeurs / chefs de projets puissent complétement valider les fonctionnalités,

  • Un serveur de prod.

Deuxièmement, je partage cette petite astuce pour utiliser SeleniumServer dans la pipeline Gitlab :

Dans l’étape Codeception, on attache une autre image docker à notre image de base, ceci grâce à la ligne :

services:
- selenium/standalone-chrome:latest

Cette image docker est fournie et maintenue par les mêmes personnes qui font SeleniumServer. Cela évite de devoir installer le serveur & les drivers à la main, ce qui est assez pratique. Dans ce cas-là, il faudra préciser dans la configuration Webdriver de Codeception le “host” qui est l’adresse de ton SeleniumServer. On peut donc se connecter sur chaque service via un alias qui est le nom de l’image simplifié. Dans notre cas, l’alias s’appelle “selenium-standalone-chrome".

Cette chaine de caractère est déclarée en tant que variable d’environnement de notre container.

Ensuite, nous avons la possibilité de récupérer dans les fichiers .yml de configuration de Codeception les variables d’environnement, ce qui permet d’avoir le même fichier pour tester en local et dans la pipeline, en faisant attention à bien créer les variables d’environnement sur ton poste.

Happy testing !

Julien Verhaeghe