· 9 min read

Intégration continue d'un projet Symfony 5 avec GitLab CI

À l'heure du DevOps, à la réduction du temps entre le développement et la mise en route des fonctionnalités, il n'est plus que jamais nécessaire de mettre en place des outils pour s'assurer de la qualité de notre code.

À l'heure du DevOps, à la réduction du temps entre le développement et la mise en route des fonctionnalités, il n'est plus que jamais nécessaire de mettre en place des outils pour s'assurer de la qualité de notre code.

Introduction

À l’heure du DevOps, à la réduction du temps entre le développement et la mise en route des fonctionnalités, il n’est plus que jamais nécessaire de mettre en place des outils pour s’assurer de la qualité de notre code.

L’intégration continue est un ensemble de pratiques utilisées en génie logiciel consistant à vérifier à chaque modification de code source que le résultat des modifications ne produit pas de régression dans l’application développée. Le concept a pour la première fois été mentionné par Grady Booch et se réfère généralement à la pratique de l’extreme programming. Le principal but de cette pratique est de détecter les problèmes d’intégration au plus tôt lors du développement. De plus, elle permet d’automatiser l’exécution des suites de tests et de voir l’évolution du développement du logiciel.

Contenu soumis à la licence CC-BY-SA 3.0.
Source : Article Intégration continue de Wikipédia en français (auteurs)

Mise en place d’un projet Symfony et de son environnement de développement

Commençons pour initier un nouveau projet Symfony.

symfony new gitlab --full
cd gitlab

Récupérons un environnement de développement avec Docker.

mkdir docker
cd docker
git clone git@gitlab.com:yoandev.co/environnement-de-developpement-symfony-5-avec-docker-et-docker-compose.git .

Adaptons l’environnement à notre projet en modifiants le fichier docker-compose.yml.

version: "3.8"
services:

    db_gitlab:
        image: mysql
        container_name: db_docker_gitlab
        restart: always
        volumes:
            - db-data:/var/lib/mysql
        environment:
            MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
        networks:
            - dev

    www_gitlab:
        build: php
        container_name: www_docker_gitlab
        ports:
          - "8080:80"
        volumes:
            - ./php/vhosts:/etc/apache2/sites-enabled
            - ../:/var/www
        restart: always
        networks:
            - dev

networks:
    dev:

volumes:
    db-data:

Il nous faut aussi modifier le fichier vhost.conf.

<VirtualHost *:80>
    ServerName localhost

    DocumentRoot /var/www/public
    DirectoryIndex /index.php

    <Directory /var/www/public>
        AllowOverride None
        Order Allow,Deny
        Allow from All

        FallbackResource /index.php
    </Directory>

    # uncomment the following lines if you install assets as symlinks
    # or run into problems when compiling LESS/Sass/CoffeeScript assets
    # <Directory /var/www/project>
    #     Options FollowSymlinks
    # </Directory>

    # optionally disable the fallback resource for the asset directories
    # which will allow Apache to return a 404 error when files are
    # not found instead of passing the request to Symfony
    <Directory /var/www/public/bundles>
        FallbackResource disabled
    </Directory>
    ErrorLog /var/log/apache2/project_error.log
    CustomLog /var/log/apache2/project_access.log combined

    # optionally set the value of the environment variables used in the application
    #SetEnv APP_ENV prod
    #SetEnv APP_SECRET <app-secret-id>
    #SetEnv DATABASE_URL "mysql://db_user:db_pass@host:3306/db_name"
</VirtualHost>

Démarrons l’environnement de développement.

docker-compose up-d

Et vérifions que tout fonctionne en allant sur http://127.0.0.1:8080/.

Modifions-le .env pour qu’il utilise notre base de donnée MySql.

DATABASE_URL="mysql://root:@db_gitlab:3306/db_name?serverVersion=5.7"

Entrons dans le bash du conteneur WWW.

docker exec -it www_docker_gitlab bash

Et créons la base de données.

php bin/console doctrine:database:create

Maintenant, nous créons une entité afin d’avoir quelque chose à tester lors de notre intégration continue ;-)

php bin/console make:entity Demo
New property name (press <return> to stop adding fields):

 Field type (enter ? to see all types) [string]:
 > string

 Field length [255]:
 > 255

 Can this field be null in the database (nullable) (yes/no) [no]:
 > no

 updated: src/Entity/Demo.php

Générons la migration et appliquons-la.

php bin/console make:migration
php bin/console doctrine:migrations:migrate

Création d’un dépôt GitLab et premier commit

Comme nous allons utiliser la CI de GitLab, nous allons devoir créer un dépôt sur GitLab. Je vous laisse créer votre dépôt seul, ce ne devrait pas être trop compliqué ;-)

Une fois le dépôt GitLab créer, il faut pousser le projet une première fois.

git init
git remote add origin git@gitlab.com:votre_namespace/votre_projet.git
git add .
git commit -m "Initial commit"
git push -u origin master

En ré-actualisant la page web de votre dépôt, vous devriez voir les fichiers de votre projet.

Écriture d’un test unitaire

L’un des objectifs de l’intégration continue c’est de jouer des tests, nous allons écrire un petit test unitaire pour pouvoir ensuite l’ajouter à notre pipeline GitLab.

php bin/console make:unit-test

 The name of the unit test class (e.g. UtilTest):
 > UnitTest

 created: tests/UnitTest.php

Éditons le fichier tests/UnitTest.php et ajoutons-y un test unitaire.

<?php

namespace App\Tests;

use App\Entity\Demo;
use PHPUnit\Framework\TestCase;

class UnitTest extends TestCase
{
    public function testDemo()
    {
        $demo = new Demo();

        $demo->setDemo('demo');

        $this->assertTrue($demo->getDemo() === 'demo');
    }
}

Et lançon notre test pour vérifier qu’il fonctionne.

php bin/phpunit
OK (1 test, 1 assertion)

Et avant de passer à la suite, on fait un petit commit (oui dans Master… c’est le mal, mais ce n’est qu’une démo hein ;-) )

git add .
git commit -m "ajout test unitaire"
git push

Mise en place du Pipeline GitLab CI

Entrons enfin de la mise en place du Pipeline GitLab par la création à la racine du projet d’un fichier .gitlab-ci.yml.

touch .gitlab-ci.yml

Nous allons mettre en place un Pipeline qui réalisera les tests suivants :

  • Check des failles de sécurité des dépendances avec Security Checker
  • Vérification du “style” de code avec PHP CS
  • Analyse statique avec PHP Stan
  • Analyse statique des fichiers Twig avec Twig-lint
  • Exécution de tests unitaires (et fonctionnels) avec PHP Unit

Nous pourrions fabriquer notre propre image Docker pour réaliser ces tests, mais je vous propose d’utiliser cette image extrêmement complète et qui vous fera gagner un temps précieux puisqu’elle contient déjà tout ce dont nous avons besoin (et bien plus) : https://github.com/jakzal/phpqa

Déclarons donc cette image dans notre Pipeline.

image: jakzal/phpqa:php7.4

Pour que nos tests fonctionne, il nous faut réaliser le composer install avant l’exécution de nos scripts de test.

before_script:
    - composer install

Et pour optimiser nous mettons en place du caching sur le répertoire vendor.

cache:
    paths:
        - vendor/

Découpons les phases de notre pipeline en trois phases. La phase CodingStandard exécutera en parallèle PHP CS, PHP Stan et Twig-lint.

stages:
    - SecurityChecker
    - CodingStandards
    - UnitTests

Mettons en place le check des failles de sécurité, et interdisons de poursuivre si cette tâche échoue.

security-checker:
    stage: SecurityChecker
    script:
        - security-checker security:check composer.lock
    allow_failure: false

Mettons en place le test qui vérifie le bon respect des règles de codage PSR-12 avec PHP CS (en excluant le fichier Kernel.php).

phpcs:
    stage: CodingStandards
    script:
        - phpcs -v --standard=PSR12 --ignore=./src/Kernel.php ./src
    allow_failure: false

Mettons en place le scan de PHP Stan.

phpstan:
    stage: CodingStandards
    script:
        - phpstan analyse ./src
    allow_failure: false

Et celui des fichiers Twig, en précisant bien le répertoire des templates.

twig-lint:
    stage: CodingStandards
    script:
        - twig-lint lint ./templates
    allow_failure: false

Il ne nous reste plus que le test unitaire et notre pipeline sera déjà bien sympathique (bon, il y a un bonus juste après).

phpunit:
    stage: UnitTests
    script:
        - php bin/phpunit
    allow_failure: false

Faisons un commit et un push pour vérifier que notre pipeline se lance bien.

git add .
git commit -m "Ajout de la pipeline"
git push

Ouvrons le menu CI/CD>Pipelines de notre dépôt GitLab, et constatons que la pipeline c’est bien exécutée (cela prend quelques minutes).

Bonus : Ajouter des tests fonctionnels avec interaction avec une base de données MySQL

Pour les plus courageux d’entre vous, nous allons ajouter à notre pipeline des tests fonctionnels, qui vont écrire réellement dans une base de données. Cela permet d’augmenter considérablement la sécurité de votre code et de vous assurer que vous n’avez pas généré un bug ou une régression.

La première chose que nous devons faire c’est mettre en place les tests fonctionnels localement dans notre projet Symfony. Nous allons devoir définir qu’elle est la base de données utilisé pour les tests. Copions le fichier .env.test en .env.test.local (nous verrons plus loin pourquoi) et éditons-le.

# .env.test.local
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
DATABASE_URL="mysql://root:@db_gitlab:3306/db_test?serverVersion=5.7"

Créons la base de données de test, et jouons-y les migrations (Bash à l’intérieur du conteneur www !!)

php bin/console doctrine:database:create --env=test
php bin/console doctrine:migrations:migrate --env=test

Générons un CRUD pour avoir de quoi tester ;-)

php bin/console make:crud

Et vérifions que cela fonctionne en ouvrant la page http://127.0.0.1:8080/demo/

Générons un test fonctionnel.

php bin/console make:functional-test

 The name of the functional test class (e.g. DefaultControllerTest):
 > FunctionalTest

 created: tests/FunctionalTest.php

Et éditons le fichier pour y écrire un premier test qui vérifie que l’url /demo répond bien.

<?php

namespace App\Tests;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class FunctionalTest extends WebTestCase
{
    public function testShouldDisplayDemoIndex()
    {
        $client = static::createClient();
        $client->followRedirects();
        $crawler = $client->request('GET', '/demo');

        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('h1', 'Demo index');
    }
}

Ajoutons un deuxième test qui charge le formulaire d’ajout.

<?php

public function testShouldDisplayCreateNewDemo()
    {
        $client = static::createClient();
        $client->followRedirects();
        $crawler = $client->request('GET', '/demo/new');

        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('h1', 'Create new Demo');
    }

Enfin ajoutons un test qui va insérer des données via le formulaire, puis vérifier que celles-ci existent bien.

<?php

    public function testShouldAddNewDemo()
    {
        $client = static::createClient();
        $client->followRedirects();
        $crawler = $client->request('GET', '/demo/new');

        $buttonCrawlerNode = $crawler->selectButton('Save');

        $form = $buttonCrawlerNode->form();

        $uuid = uniqid();

        $form = $buttonCrawlerNode->form([
            'demo[demo]'    => 'Add Demo For Test' . $uuid,
        ]);

        $client->submit($form);

        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('body', 'Add Demo For Test' . $uuid);
    }

Pour terminer, validons que nos tests fonctionnent.

php bin/phpunit --testdox

PHPUnit 7.5.20 by Sebastian Bergmann and contributors.

Testing Project Test Suite
App\Tests\Functional
 Should display demo index
 Should display create new demo
 Should add new demo

App\Tests\Unit
 Demo

Time: 286 ms, Memory: 32.00 MB

OK (4 tests, 10 assertions)

Il ne nous reste plus qu’a modifier quelques fichiers pour faire fonctionner les tests fonctionnels dans notre pipeline GitLab.

Commençons par éditer notre fichier .gitlab-ci.yml pour y ajouter une base de donnée, et lui passer les commandes de création de la base de données. Au passage on en profite pour utiliser une image Docker plus standard pour cette étape (et en y installant que le nécessaire : git, composer, zip, pdo_mysql…)

#Extrait du fichier.gtilab-ci.yml

phpunit:
    image: php:7.4-apache
    stage: UnitTests
    services:
        - name: mysql:5.7
          alias: mysql
    variables:
      MYSQL_ROOT_PASSWORD: pass_test
      MYSQL_DATABASE: myapptest
      MYSQL_USER: myapptest
      MYSQL_PASSWORD: myapptest
      DATABASE_URL: 'mysql://myapptest:myapptest@mysql:3306/myapptest'
    before_script:
        - apt-get update && apt-get install -y git libzip-dev
        - curl -sSk https://getcomposer.org/installer | php -- --disable-tls && mv composer.phar /usr/local/bin/composer
        - docker-php-ext-install mysqli pdo pdo_mysql zip
        - php bin/console doctrine:database:drop --force --env=test
        - php bin/console doctrine:database:create --env=test
        - php bin/console doctrine:migration:migrate --env=test --no-interaction
    script:
        - php bin/phpunit
    allow_failure: false

Éditons le fichier .env.test pour indiquer à Symfony qu’elle base de données il doit utiliser dans le contexte d’exécution des tests.

# .env.test
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
DATABASE_URL=mysql://myapptest:myapptest@mysql:3306/myapptest

Ultime étape, on commit et on push !

git add .
git commit -m "Ajout des tests fonctionnels"
git push

Vous devriez peut-être avoir un le fichier /src/Controller/DemoController.php qui ne passe pas le contrôle PSR-12 de PHP CS, je vous laisse faire la modification signalée (il manque des espaces autour d’un ”.” en ligne 86) et refaire un commit + push.

Il ne nous reste plus qu’à admirer le spectacle sur Gitlab ;-)

Conclusions

À travers ce (trop) long article nous avons mis en place un pipeline d’intégration continue relativement complet, surtout si vous y ajoutez des tests unitaires et fonctionnels.

GitLab CI, avec l’utilisation d’images Docker nous offre une CI vraiment pratique et modulable, et une fois le principe compris, plein d’autres possibilités s’ouvre à vous!

Vous trouverez les sources sur ce dépôt GitLab.

Back to Blog