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 :

  1. pourquoi cette variable ?
  2. ton nom de fonction est étrange.
  3. tu pourrais utiliser cette fonction.
  4. j’ai déjà codé un truc ressemblant.
  5. 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é :

  1. nom de variable
  2. taille du code
  3. ré-utilisation
  4. psr
  5. 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:

  1. Ne pas avoir de commentaire vide
  2. Le nombre de sélecteurs max
  3. Vérification des accolades
  4. 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.

Partie WebPerformance

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.