La qualité est un vaste sujet, surtout quand on l'associe au développement d'application web.
Ce qui est encore plus compliqué, c'est de mettre en place l'environnement d'intégration continue (CI) de suivi de qualité.
Pendant plus de 2 ans, nous avons mis en place une CI de qualité chez LeMonde.fr qui a évolué en fonction de nos besoins. Le but de cet article est de comprendre la stratégie et les technos choisies pour la CI d'un site comme LeMonde.fr
La partie Symfony
Comme pour tout langage de programmation, la première chose que l'on veut vérifier, c'est la syntaxe. La première chose à mettre en place est donc un vérificateur de syntaxe en PHP.
php -l somefile.php
Maintenant, il faut savoir quand faire cette vérification. L'idée numéro 1 étant de laisser le développeur le faire avant d'envoyer son code sur git. Si l'appel n'est pas automatisé, 1 fois sur 3, le développeur ne lance pas la commande.
L'idée est donc de le faire à chaque commit. Pour cela rien de plus simple, on ajoute un hook de pre-commit.
Dans le fichier .git/hooks/pre-commit
il faut ajouter le code suivant.
#!/bin/sh
BADWORDS='var_dump|die|todo'
EXITCODE=0
FILES=`git diff --cached --diff-filter=ACMRTUXB --name-only $against --`
for FILE in $FILES ; do
if [ "${FILE##*.}" = "php" ]; then
php -l "$FILE"
if [ $? -gt 0 ]; then
EXITCODE=1
fi
grep -H -i -n -E "${BADWORDS}" $FILE
if [ $? -eq 0 ]; then
EXITCODE=1
fi
fi
done
if [ $EXITCODE -gt 0 ]; then
echo
echo 'Fix the above errors or use:'
echo ' git commit --no-validate'
echo
fi
exit $EXITCODE
Si tout est ok, lors de chaque commit
, le hook va vérifier la syntaxe php.
Une fois cela validé, la suite logique est de faire en sorte que les développeurs codent tous avec les mêmes standards. Ce qui est bien, c'est que PHP a déjà des standards : les PSR.
Encore faut-il que tous les développeurs les suivent, c'est assez simple en PHP. Nous avons ajouté dans notre hook de pre-commit la commande de vérification disponible dans cet article, vérifier la qualité du code
Nous étions satisfaits mais c'était assez contraignant de passer par les hooks git. La première difficulté était que chaque développeur pouvait changer ses hooks, ce qui peut poser des problèmes.
Nous avons donc regardé les solutions du marché, et comme nous utilisions Github, Travis était la plus approprié.
La migration était simple, nous avons ajouté le fichier .travis.yml
dans notre projet contenant les mêmes scripts de vérification.
before_script:
- ! find . -type f -name "*.php" -exec php -d error_reporting=32767 -l {} \; 2>&1 >&- | grep "^"
La vérification ne se faisait que lors d'une pull request. Il fallait aider le développeur à voir les erreurs de coding style et syntaxe avant, c'est-à-dire pendant son développement.
Nous avons choisi d'utiliser l'editorconfig ! L'editorconfig est un fichier que l'on ajoute à la racine du repo et qui est utilisé par la plupart des IDE pour vérifier en live la syntaxe.
Exemple de fichier .editorconfig
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*.md]
trim_trailing_whitespace = false
indent_style = tabs
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_size = 4
indent_style = space
[*.{js,jsx,json}]
indent_size = 2
indent_style = space
[Makefile]
indent_size = 4
indent_style = tabs
La syntaxe c'est fait !!!!
Passons au code ! Comme tout le monde, nous avions des tests unitaires et fonctionnels en phpunit. Comme nous avions Travis, qui était en place, il fallait seulement ajouter le script pour les lancer dans la configuration.
//.travis.yml
script:
## PHPUnit
- vendor/bin/phpunit
Bravo vos tests sont dans la CI !!!
Après cette importante étape, nous avons cherché à savoir ce qui nous manquait. Nous avions des tests, mais cela n'attestait pas de la qualité de notre code, seulement du fait qu'il était fonctionnel. Pour améliorer la qualité du code, nous avons alors instauré l'obligation d'une relecture par deux autres développeurs de chaque pull request afin qu'elle soit validée. Nous avions la sensation que la qualité était meilleure car les gens posaient les bonnes questions :
- pourquoi cette variable ?
- ton nom de fonction est étrange.
- tu pourrais utiliser cette fonction.
- j'ai déjà codé un truc ressemblant.
- etc...
Mais il nous est arrivé que certaines pull requests nous aient posé des problèmes. Des questions du type for
ou while
sont apparues dans les codes review et cela est devenu trollLand sur certaines pull requests. Comment faire ?
L'idée fut d'avoir un juge de touche Nous avons cherché et nous avons choisi Scrutinizer. Scrutinizer est une solution qui permet de juger votre code. Il vous donne une note en prenant en compte plusieurs signes de qualité :
- nom de variable
- taille du code
- ré-utilisation
- psr
- etc...
La mise en place est simple puisque Scrutinizer se plugue facilement à Github. Il suffit d'ajouter un fichier .scrutinizer.yml
dans votre projet.
Exemple:
checks:
php:
verify_property_names: true
verify_argument_usable_as_reference: true
verify_access_scope_valid: true
variable_existence: true
useless_calls: true
use_statement_alias_conflict: true
use_self_instead_of_fqcn: true
uppercase_constants: true
unused_variables: true
unused_properties: true
unused_methods: true
unused_parameters: true
unreachable_code: true
too_many_arguments: true
symfony_request_injection: true
switch_fallthrough_commented: true
sql_injection_vulnerabilities: true
single_namespace_per_use: true
simplify_boolean_return: true
side_effects_or_types: true
security_vulnerabilities: true
return_doc_comments: true
return_doc_comment_if_not_inferrable: true
require_scope_for_properties: true
require_scope_for_methods: true
require_php_tag_first: true
remove_extra_empty_lines: true
psr2_switch_declaration: true
psr2_class_declaration: true
property_assignments: true
properties_in_camelcaps: true
prefer_while_loop_over_for_loop: true
precedence_mistakes: true
precedence_in_conditions: true
phpunit_assertions: true
php5_style_constructor: true
parse_doc_comments: true
parameters_in_camelcaps: true
parameter_non_unique: true
parameter_doc_comments: true
param_doc_comment_if_not_inferrable: true
overriding_private_members: true
optional_parameters_at_the_end: true
one_class_per_file: true
non_commented_empty_catch_block: true
no_unnecessary_if: true
no_unnecessary_final_modifier: true
no_underscore_prefix_in_properties: true
no_underscore_prefix_in_methods: true
no_trait_type_hints: true
no_trailing_whitespace: true
no_short_variable_names:
minimum: '3'
no_short_open_tag: true
no_short_method_names:
minimum: '3'
no_property_on_interface: true
no_non_implemented_abstract_methods: true
no_new_line_at_end_of_file: true
no_long_variable_names:
maximum: '20'
no_goto: true
no_global_keyword: true
no_exit: true
no_eval: true
no_error_suppression: true
no_empty_statements: true
no_duplicate_arguments: true
no_debug_code: true
no_commented_out_code: true
newline_at_end_of_file: true
more_specific_types_in_doc_comments: true
naming_conventions:
local_variable: '^[a-z][a-zA-Z0-9]*$'
abstract_class_name: ^Abstract|Factory$
utility_class_name: 'Utils?$'
constant_name: '^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$'
property_name: '^[a-z][a-zA-Z0-9]*$'
method_name: '^(?:[a-z]|__)[a-zA-Z0-9]*$'
parameter_name: '^[a-z][a-zA-Z0-9]*$'
interface_name: '^[A-Z][a-zA-Z0-9]*Interface$'
type_name: '^[A-Z][a-zA-Z0-9]*$'
exception_name: '^[A-Z][a-zA-Z0-9]*Exception$'
isser_method_name: '^(?:is|has|should|may|supports)'
line_length:
max_length: '120'
method_calls_on_non_object: true
missing_arguments: true
align_assignments: true
argument_type_checks: true
assignment_of_null_return: true
avoid_aliased_php_functions: true
avoid_closing_tag: true
avoid_conflicting_incrementers: true
avoid_corrupting_byteorder_marks: true
avoid_duplicate_types: true
avoid_entity_manager_injection: true
avoid_fixme_comments: true
avoid_length_functions_in_loops: true
avoid_multiple_statements_on_same_line: true
avoid_perl_style_comments: true
avoid_superglobals: true
avoid_todo_comments: true
avoid_unnecessary_concatenation: true
avoid_usage_of_logical_operators: true
avoid_useless_overridden_methods: true
blank_line_after_namespace_declaration: true
catch_class_exists: true
classes_in_camel_caps: true
closure_use_modifiable: true
closure_use_not_conflicting: true
code_rating: true
deadlock_detection_in_loops: true
deprecated_code_usage: true
duplication: true
encourage_postdec_operator: true
encourage_shallow_comparison: true
encourage_single_quotes: true
fix_doc_comments: true
fix_line_ending: true
fix_use_statements:
remove_unused: true
preserve_multiple: false
preserve_blanklines: false
order_alphabetically: true
foreach_traversable: true
foreach_usable_as_reference: true
instanceof_class_exists: true
function_in_camel_caps: true
tools:
external_code_coverage:
timeout: 600
build_failure_conditions:
- 'elements.rating(<= D).new.exists'
- 'issues.label("coding-style").new.exists'
- 'issues.severity(>= MINOR).new.exists'
- 'project.metric_change("scrutinizer.test_coverage", < -0.10)'
- 'patches.label("Doc Comments").exists'
Vous pouvez configurer énormément de choses mais surtout les conditions d'acceptation. Comme vous le voyez dans la configuration, on y trouve build_failure_conditions
qui permet de mettre les seuils d'acceptation de la pull request.
Scrutinizer permet aussi de gérer le taux de code coverage, il faut alors l'envoyer à Scrutinizer à partir de la sortie de Travis. Un tutoriel est disponible ici. Normalement, il vous faut ajouter ceci dans le fichier .travis.yml
after_script:
## Scrutinizer
- wget https://scrutinizer-ci.com/ocular.phar
- php ocular.phar --access-token="TOKEN" code-coverage:upload --format=php-clover ./build/logs/clover.xml
Scrutinizer est assez complet et permet de suivre la qualité de votre code au fil du temps, en vous envoyant des mails de suivi et vous proposant des dashboards.
Encore une étape de terminée !!!!
Nous avons utilisé cette stack pendant plus d'un an, nous étions assez satisfait. Puis un jour, un article sur les tests de mutation, nous a donné envie d'aller plus loin. Nous avons alors essayé les tests de mutation, ce qui nous a permis de voir que même avec un code coverage de 90%, il y avait des tests qui ne faisaient rien ou qui testaient mal le code. Après avoir fait les changements, nous voulions aussi l'introduire dans notre CI. Le premier réflexe étant d'ajouter le script dans la configuration travis. Grosse erreur, le script mettant plus de 20 minutes sur notre projet, nous avions les jobs Travis en attente sur les autres projets. Mais heureusement, Travis avait sorti une nouvelle fonctionnalité qui permet de lancer les jobs en mode CRON et donc de le faire qu'une fois par jour, ce qui est suffisant pour ce genre de test. Il nous suffsait alors d'ajouter la config suivante.
//.travis.yml
script:
- |
if [[ "$TRAVIS_EVENT_TYPE" == 'cron' && "$TRAVIS_BRANCH" == 'master' ]]; then
php bin/humbug
php bin/humbug stats ./build/humbug/log.json --skip-killed=yes -vvv
fi
Il suffit alors de regarder chaque jour le build humbug fait sur la branche master.
La dernière étape de la partie php est terminée !!!!
La partie javascript
L'architecture évoluant, nous avons dû nous adapter et donc travailler de plus en plus avec du javascript.
Nous avons alors réfléchi à la même problématique, du javascript oui, mais de qualité.
Encore une fois, nous avons commencé par la syntaxe avec la mise en place Eslint. Nous avons choisi comme standard la configuration de airbnb.
---
extends: "airbnb"
env:
node: true
browser: true
jest: true
settings:
import/resolver:
webpack:
config: 'app/config/webpack.config.js'
Puis nous avons ajouté la vérification à la configuration de Travis.
//.travis.yml
script:
- eslint src app/config --ext .js --ext .jsx
Comme nous faisons déjà du javascript dans le frontend, notre Editorconfig était correctement configuré.
Étape 1, 3 secondes !!!
Nous avons alors fait les tests unitaires. Comme toujours le choix de la techno ne fut pas simple. Mais comme nous faisions du react, jest s'est très vite imposé.
Une fois les tests développés, nous avons encore une fois ajouté l'appel dans la configuration Travis.
//.travis.yml
script:
- jest
Étape 2, done !!!!
Comme nous le savions de notre expérience en PHP, tout cela ne suffisait pas. Nous avons donc cherché un outil équivalent à Scrutinizer et nous avons trouvé Bithound. Cet outil est un peu moins poussé, mais il permet de mettre une note sur votre code et surtout de vous alerter quand des librairies extérieures ne sont plus à jour.
La configuration est comme toujours un fichier à la racine du projet.
//.bithoundrc
{
"ignore": [
"**/node_modules/**"
],
"test": [
"**/*.spec.js*"
],
"critics": {
"lint": {
"engine": "eslint"
}
}
}
Bithound n'est pas mal mais n'apporte pas les mêmes fonctionnalités que Scrutinizer. Il n'y a pas de dashboard de suivi et les checks sont limités.
Et voila, votre javascript est maintenant de qualité !!!
Nous n'avons malheureusement pas trouvé une technologie permettant de faire des tests de mutation avec Jest, donc nous n'avons pas passé cette étape sur le code javascript. (Avez-vous des solutions ?)
Partie CSS
On l'oublie souvent mais le CSS, c'est aussi du code, et la qualité de celui-ci doit aussi être prise en compte.
Jamais deux sans trois, on commence par la syntaxe. Pour cela, nous avons utilisé Stylelint qui permet de gérer la syntaxe de vos fichiers CSS. Stylelint permet de nombreuses vérifications:
- Ne pas avoir de commentaire vide
- Le nombre de sélecteurs max
- Vérification des accolades
- etc...
Si vous suivez le tutoriel, vous devez écrire un fichier de configuration à la racine de votre repository.
Exemple de fichier .stylelintrc
{
"plugins": [
"stylelint-order"
],
"rules": {
"color-hex-case": "lower",
"color-no-invalid-hex": true,
"font-weight-notation": "named-where-possible",
"indentation": 4,
"function-max-empty-lines": 2,
"function-comma-space-after": "always-single-line",
"function-parentheses-space-inside": "never-single-line",
"block-closing-brace-newline-after": [
"always-multi-line",
{ "ignoreAtRules": ["if", "else"] }
],
"number-leading-zero": "always",
"number-no-trailing-zeros": true,
"number-max-precision": 6,
"block-no-empty": true,
"comment-no-empty": true,
"declaration-bang-space-before": "always",
"declaration-block-no-duplicate-properties": [
true,
{ "ignore": ["consecutive-duplicates"] }
],
"string-quotes": "single",
"max-line-length": 100,
"max-empty-lines": 2,
"max-nesting-depth": [3,
{ "ignoreAtRules": ["if", "else", "include"] }
],
"order/declaration-block-order": [
"custom-properties",
"dollar-variables",
{
"type": "at-rule",
"name": "include",
"hasBlock": false
},
"declarations",
{
"type": "at-rule",
"name": "include",
"parameter": "to-screen",
"hasBlock": true
},{
"type": "at-rule",
"name": "include",
"parameter": "from-screen",
"hasBlock": true
},{
"type": "at-rule",
"name": "include",
"parameter": "from-to-screen",
"hasBlock": true
},{
"type": "at-rule",
"name": "include",
"parameter": "at-screen",
"hasBlock": true
},
"rules"
],
"order/declaration-block-properties-specified-order" : [
[
"box-sizing",
"display",
"float",
"flex",
"flex-flow",
"flex-basis",
"align-self",
"align-items",
"order",
"position",
"top",
"right",
"bottom",
"left",
"min-width",
"z-index",
"width",
"max-width",
"min-height",
"height",
"max-height",
"overflow",
"overflow-y",
"overflow-x",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left"
],
{
"unspecified": "bottom"
}
]
}
}
Pour lancer ceci dans nos CI, nous avons utilisé le plugin pour gulp et donc ajouté une tâche gulp.
gulp.task('scss:lint', () => {
const lintPlugins = [
stylelint(),
reporter({
clearReportedMessages: true,
}),
];
return gulp.src(path.resolve(src.scss, '**/*.scss'))
.pipe(postcss(lintPlugins, { syntax }).on('error', onError));
});
Et comme à chaque fois, il nous faut ajouter la commande dans le fichier .travis.yml
.
script:
- gulp scss:lint
La seconde façon de vérifier la qualité du CSS est de faire des tests de non régression visuelle. Pour cela BackstopJs est une solution complète, qui permet de tester deux versions de votre code html/css.
L'idée est de générer une version statique des pages de votre site avec votre code actuel et de stocker le résultat. Ensuite, vous pouvez faire des modifications de votre code et lancer les tests de régression visuelle, cela génère un site qui vous montre les différences entre les deux versions. À vous de décider si la différence est normale ou non.
Nous n'avons pas ajouté cette vérification dans la partie automatique de la CI, parce que souvent les tests montrent des différences normales et non des erreurs. Mais pour rendre ceci plus simple, nous avions généré le site statique de BackstopJs lors d'un merge dans master. Pour cela, nous utilisions la variable d'environnement de travis qui permet de connaître la branche, si celle-ci était un tag, nous déployions le site dans un bucket lisible par tout le monde et qui permettait de faire les vérifications manuelles.
Voici l'exemple de configuration Travis.
language: node_js
node_js:
- 6.7.0
sudo: false
cache:
directories:
- node_modules
script:
- make lint
install:
- make install
- mkdir -p "deploy/${TRAVIS_TAG}"
- cp -R public/assets "deploy/${TRAVIS_TAG}"
deploy:
provider: gcs
access_key_id: GOOGLE_ID
secret_access_key:
secure: SECURE
bucket: BUCKET_NAME
acl: public-read
local-dir: deploy
skip_cleanup: true
on:
repo: lemonde/pattern-guides
branch: master
tags: true
condition: $TRAVIS_TAG =~ [0-9]+\.[0-9]+\.[0-9]+
Nous avions avec cela gagné en qualité CSS et avons eu beaucoup moins de régression visuelle.
Parce que la WebPerformance, c'est aussi de la qualité, nous avons ajouté un outil dans notre CI pour suivre cette dernière.
Nous avons choisi Speedcurve, un outil de monitoring simple d'utilisation et surtout qui permet de suivre les concurrents.
Speedcurve permet beaucoup de choses, nous avons essayé d'utiliser l'ensemble des fonctionnalités disponibles.
La première chose est de suivre la WebPerformance de votre site en production. Speedcurve va, selon votre configuration, se connecter plusieurs fois dans la journée sur votre site et faire des calculs de WebPerformance.
Speedcurve vous renvoie plusieurs indicateurs très sympas :
- Start Render
- Speed Index
- Visually Complete
- Page Load Time
- css size
- Image Size
- etc ...
Ce qui est intéressant avec cette fonctionnalité, c'est de pouvoir suivre jour après jour votre WebPerformance.
La seconde chose est que vous pouvez mettre d'autres sites que le votre et donc vérifier votre WebPerformance par rapport aux autres.
La dernière fonctionnalité importante permet de lancer une demande de vérification via une API. Nous avions alors, après chaque mise en production lancée, un check sur Speedcurve en le nommant avec le numéro de la release mise en prod. Ceci permet ensuite de voir sur l'ensemble des dashboards disponible sur Speedcurve le moment de la mise en production, mais aussi de faire des comparaisons entre les releases.
Conclusion
L'utilisation de l'ensemble des outils nous a permis de suivre la qualité de notre site dans le temps. Ceci permet d'éviter la dette technique qui peut très vite s'accumuler. Effectivement, cela prend du temps de mettre tous les outils en place, mais il faut savoir le prendre pour en gagner après. Nous avons mis plus de deux ans pour avoir l'ensemble des outils, allez y pas à pas et vous y arriverez.