Créer un navigateur personnalisé - 9ème partie

Automatisation des tests avec Mocha, vérification syntaxique avec ESLint et intégration continue avec Travis CI

27 avril 2016

Introduction

Ce projet est open source et accessible depuis GitHub GL-Browser.

Cet article est le 9ème d'une série d'articles concernant la création d'un navigateur internet incluant par défaut des fonctionnalités que l'on retrouve habituellement sous forme d'extensions dans Chrome ou Firefox.

Exemples de fonctionnalités :

Nous souhaitons :

Nous utilisons Electron qui mixe le navigateur open source Chromium avec la richesse du framework Node.js.

Objectifs

Lors de chaque modification du code source, il est fastidieux et peu fiable de revalider le bon fonctionnement du logiciel avec des tests manuels.

Dans ce 9ème article, nous automatiserons l'exécution des tests à chaque modification du code source.

Résumé des articles précédents

Article 1: initialisation du projet avec Electron.
Article 2: injection du CSS et du JavaScript dans la page de Google.
Article 3: injection de scripts JavaScript et de styles CSS suivant l'URL.
Article 4: bloquer les requêtes de suivi/tracking des utilisateurs.
Article 5: amélioration de l'interface utilisateur avec Bootstrap.
Article 6: encapsulation des composants visuels avec Riot.
Article 7: intégration d'un gestionnaire de mots de passe.
Article 8: intégration d'un gestionnaire de favoris.

Pour récupérer directement les sources du 9ème article :

git clone https://github.com/emmanuelroecker/GL-Browser
git checkout article9

Automatiser les tests

Installer Mocha

Mocha est un outil libre permettant de structurer la rédaction et l'exécution de tests automatiques.
Chaque test est validé à l'aide d'une assertion de la librairie assert.

Le package electron-mocha permet d'utiliser Mocha dans l'environnement d'Electron :

npm install --save-dev electron-mocha

Une fois installé, le fichier package.json contient :

{
  "dependencies": {
      "app-module-path": "^1.0.6",
      "bootstrap": "^3.3.0",
      "commander": "^2.9.0",
      "electron-prebuilt": "^0.37.0",
      "jquery": "^1.9.1",
      "js-yaml": "^3.5.0",
      "match-pattern": "^0.0.1",
      "riot": "^2.3.0"
    },
    "devDependencies": {
      "electron-mocha": "^1.2.1"
    }
}

La commande test ajoutée à package.json exécutera l'ensemble des fichiers JavaScript du répertoire tests :

{
    "scripts": {
        "test": "electron-mocha --recursive ./tests",
        "start": "electron main.js",
    }
}

Pour l'exécuter :

npm test

Tester les librairies

L'ensemble des tests des librairies se trouve dans le répertoire tests/libs.

Un fichier JavaScript est créé pour chacune des librairies à tester.

Chaque fichier contient une fonction describe explicitant la librairie testée ainsi qu'un ensemble de fonctions it.
Chaque fonction it teste une méthode ou une fonctionnalité de la librairie.

Les assertions utilisées sont par exemple :

Ainsi, pour tester la fonction de calcul d'une empreinte de la classe cryptClass, le fichier tests/libs/crypt.js contient :

const cryptClass = require('libs/crypt/crypt.js');
const assert = require('assert');
const fs = require('fs');

describe('cryptClass', function () {    //description de la librairie testée
    it('hash string', function () {     //teste l'empreinte d'une chaîne de caractère
        let crypt = new cryptClass();
        assert.equal('A6eLJscKw4eCn7o5CHaKTO/9lox5z+H+t78wjUnT8n4=', crypt.hash('masterpassword'));
    });
    it('hash file utf8', function () {  //teste l'empreinte d'un fichier au format utf-8
        let crypt = new cryptClass();
        let data = fs.readFileSync('./tests/data/customize/google/customize.css', 'utf8');
        assert.equal('7fZH9pzfmQh3yQk1UXP7pWgUoYRXHjbbBuHKIltsyXY=', crypt.hash(data)); 
    });
}

En exécutant en ligne de commande :

npm test

le résultat affiche le détail des tests effectués avec succès ou en échec :

cryptClass
  √ hash string
  1) hash file utf8

1 passing (0s)
1 failing

1) cryptClass hash file utf8:

   AssertionError: '8fZH9pzfmQh3yQk1UXP7pWgUoYRXHjbbBuHKIltsyXY=' == '7fZH9pzfmQh3yQk1UXP7pWgUoYRXHjbbBuHKIltsyXY='
   + expected - actual

   -8fZH9pzfmQh3yQk1UXP7pWgUoYRXHjbbBuHKIltsyXY=
   +7fZH9pzfmQh3yQk1UXP7pWgUoYRXHjbbBuHKIltsyXY=

Tester les composants visuels

Les composants visuels peuvent être simulés en exécutant les tests dans une page de rendu HTML d'Electron appelé renderer.
Pour cela, l'option --renderer est ajoutée à electron-mocha dans le fichier package.json :

{
    "scripts": {
        "test": "electron-mocha --renderer --recursive ./tests",
        "start": "electron main.js",
    }
}

electron-mocha effectue tous les tests dans le même renderer, il est donc nécessaire de supprimer le contenu de la balise body avant chaque test à l'aide de la fonction beforeEach de Mocha:

describe('raw riot', function () {
    beforeEach(function () {
        document.body.innerHTML = ''; //supprime le contenu de la balise body
    });
    it('test1', function () {
        ...
    });
    it('test2', function () {
        ...
    });
}

La fonction before de Mocha est utilisée pour ajouter le code source du composant visuel dans la page HTML avant chaque groupe de tests :

describe('raw riot', function () {
    before(function() {
        let rawTag = fs.readFileSync('./components/UX/raw.riot.tag', 'utf8'); //lecture du composant riot
        eval(riot.compile(rawTag));                                           //compile et exécute le code du composant riot
    });
    beforeEach(function () {
        document.body.innerHTML = '';
    });
    it('test1', function () {
        ...
    });
    it('test2', function () {
        ...
    });
}

Pour tester par exemple le bon fonctionnement du composant tags/raw.riot.tag qui permet d'afficher du code HTML sans échappement :

describe('raw riot', function () {                                            //description du composant testé
    before(function() {
        let rawTag = fs.readFileSync('./components/UX/raw.riot.tag', 'utf8'); //lecture du composant riot
        eval(riot.compile(rawTag));                                           //compile et exécute le composant riot
    });
    beforeEach(function () {
        document.body.innerHTML = '';
    });
    it('test', function () {
        let html = document.createElement('raw');   //créé la balise html <raw>
        document.body.appendChild(html);            //ajoute la balise <raw> à la page html
        riot.mount('raw', {                         //monte le composant riot ...
            content: '<div><b>test</b></div>'       //... avec un paramètre
        })[0];
        assert.equal('test', document.querySelector('raw').textContent);    //vérifie à l'aide d'une assertion que la balise <raw> contient ce que l'on souhaite
    });
}

Remarques

Il est possible de simuler le clique de souris sur un bouton :

let button = document.querySelector('button');
button.click();

ainsi que la frappe sur une touche du clavier :

let keycode = 13;  //code de la touche à simuler
let eventObj = document.createEventObject ? document.createEventObject() : document.createEvent('Events');

if (eventObj.initEvent) {
    eventObj.initEvent(type, true, true);
}

eventObj.keyCode = keyCode;
eventObj.which = keyCode;

element.dispatchEvent ? element.dispatchEvent(eventObj) : element.fireEvent('on' + type, eventObj);

Vérification syntaxique

ESLint permet de vérifier syntaxiquement le code source JavaScript de notre projet :

ESLint est installé en exécutant dans le répertoire du projet :

npm install --save-dev eslint

Une fois installé, le fichier package.json est automatiquement mis à jour et contient :

{
  "dependencies": {
    "app-module-path": "^1.0.6",
    "bootstrap": "^3.3.0",
    "commander": "^2.9.0",
    "electron-prebuilt": "^0.37.0",
    "jquery": "^1.9.1",
    "js-yaml": "^3.5.0",
    "match-pattern": "^0.0.1",
    "riot": "^2.3.0"
  },
  "devDependencies": {
    "electron-mocha": "^1.2.1",
    "eslint": "^2.8.0"
  }
}

Un fichier .eslintrc créé à la racine du projet contient les règles syntaxiques à tester :

{
    "rules": {
        "no-console":0,
        "indent": [
            2,
            "tab"
        ],
        "quotes": [
            2,
            "single"
        ],
        "linebreak-style": [
            2,
            "unix"
        ],
        "semi": [
            2,
            "always"
        ]
    },
    "env": {
        "es6": true,
        "node": true
    },
    "extends": "eslint:recommended"
}

Pour exécuter la vérification syntaxique puis les tests, le fichier package.json est mis à jour :

{
    "scripts": {
        "test": "npm run lint && npm run mocha",
        "start": "electron app.js",
        "lint": "eslint .",
        "mocha": "electron-mocha --renderer --recursive ./tests/libs ./tests/tags --require ./tests/setup.js "
    }
}

Pour exécuter tous les tests :

npm test

Intégration continue

Travis CI est couplé avec GitHub pour lancer automatiquement les tests à chaque modification du code source.

L'utilisation de Travis CI est gratuite pour les projets open source.

Lors d'un git-push sur le dépôt hébergé par GitHub, Travis CI crée une machine virtuelle sur laquelle est cloné le projet et exécute automatiquement les tests.

Un badge vert indique si les tests ont été correctement effectués, rouge dans le cas contraire.

Pour configurer l'utilisation de Travis CI, le fichier .travis.yml à la racine du projet contient :

language: node_js
node_js:
  - "5"
before_script:
  - export DISPLAY=:99.0; sh -e /etc/init.d/xvfb start

Le dépôt du projet GitHub est activé depuis l'interface de Travis CI :

Flick repository on Travis

Travis CI permet de visualiser la console de la machine virtuelle :

Capture travis ci

Le lien du badge de résultat des tests est ajouté dans le fichier readme.md du projet :

[![Build Status](https://travis-ci.org/emmanuelroecker/GL-Browser.svg?branch=article9)](https://travis-ci.org/emmanuelroecker/GL-Browser)

Résultat

Build Passing Badge

Prochain article

Le prochain article traitera de la couverture de code.