· 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.
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.