Publié par

Il y a 6 années -

Temps de lecture 10 minutes

Introduction aux tests unitaires en javascript

De la touche "F5" aux frameworks de tests

Les tests unitaires sont aujourd’hui une norme dans le développement des applications Java. L’amplification des techniques Agiles et du mouvement Software Craftsmanship ont poussé à mettre les tests unitaires comme prérequis au développement d’applications.
 
Concernant le développement d’applications front en javascript, les tests se limitent souvent à une vérification manuelle du comportement attendu sur le navigateur. La touche “F5” et les “alert” en sont les principaux outils. Qui n’a pas passé des heures à rafraîchir sa page et à regarder les messages d’alertes pour comprendre le changement de comportement d’un module lors d’une modification du code ?
 
Ceci entraîne une perte de temps énorme, la confiance envers son code lorgnant vers 0 ainsi qu’une maintenance qui devient quasiment impossible.
 
Heureusement, au fil du temps les navigateurs ont sorti des outils permettant de faciliter les tests du code tels que Firebug pour Firefox ou le puissant outil de développement présent dans Chrome (je n’ai malheureusement pas encore utilisé les outils IE).  Ces outils sont d’une aide inestimable mais les tests sont toujours fait manuellement.
Des frameworks de tests unitaires pour Javascript existent pourtant depuis plus de 10 ans. L’apparition de Node.js et de frameworks de développement front en javascript (Angular, Backbone, Ember…) ont fait passer le javascript dans une dimension supérieure et permettent aujourd’hui le développement partiel ou total d’applications dans ce langage. Ce développement impose de facto l’utilisation de tests (unitaires, d’intégration…).
 
Nous allons voir dans cet article la logique des tests unitaires en javascript pur afin de mieux appréhender le fonctionnement des frameworks de tests automatisés tel que Jasmine, mocha ou QUnit. Nous montrerons d’ailleurs une version de ces tests avec QUnit afin de commencer à voir les possibilités que peuvent nous offrir ces frameworks.

Mon premier test unitaire

 
Prenons une fonction javascript simple qui convertit un montant donné en euros en un montant en dollars et appelons-la, disons, euroToDollar. À l’heure où ces lignes sont écrites, 1 € vaut (environ) 1,30 $. J’aimerais donc vérifier que quand on donne 1 à la fonction, celle-ci me renvoie 1,30.

function euroToDollar(euro){
 return 1.3;
}

Dans une version très simple, je pourrais faire : console.log(euroToDollar(1)) et vérifier que j’ai bien 1,30.
 
Pour ce faire j’écris un fichier de tests et intégrons le dans une page html, comme ceci :

console.log(euroToDollar(1));
console.log(euroToDollar(2));
<!doctype html>
<html>
 <head>
  <meta charset="utf-8">
  <title>euroToDollar tests</title>
 </head>
 <body>
  <script type="text/javascript" src="euroToDollar.js"></script>
  <script type="text/javascript" src="euroToDollar-test.js"></script>
 </body>
</html>

J’ai maintenant une petite série de tests qui me permet de voir que 1€ retourne bien 1,30$ mais que 2€ ne nous retourne pas 2,60$.
 
Je modifie ma fonction en conséquence :

function euroToDollar(euro){
 return euro * 1.3;
}

Voici notre premier test unitaire. Vous avez sûrement déjà fait cela un grand nombre de fois pour vérifier le comportement de vos fonctions.
Nous devons cependant toujours contrôler manuellement nos données. Pour automatiser la vérification, nous pouvons nous reposer sur le principe des assertions.
 
L’assertion est le coeur du test unitaire. Il permet au système de déterminer si un test s’est déroulé avec succès ou non.
 
J’écris donc une fonction d’assertion simple et l’utilise pour nos tests :

function assert(message, expr){
 if(!expr){
  throw new Error(message);
 }
}

Je voudrais aussi pouvoir afficher un message en vert si le test s’est bien passé, en rouge sinon.

function assert(message, expr){
 if(!expr){
  output(false, message);
  throw new Error(message);
 }
 output(true, message);
}

function output(result, message){
 var p = document.createElement('p');
 message += result ? ' : SUCCESS' : ' : FAILURE';
 p.style.color = result ? '#0c0' : '#c00';
 p.innerHTML = message;
 document.body.appendChild(p);
}



assert('1€ should return 1,3$', euroToDollar(1) === 1.3);
assert('2€ should return 2,6$', euroToDollar(2) === 2.6);

Et voilà ! Je peux maintenant tester ma fonction dans une page web et afficher un message en vert si le test a réussi ou en rouge sinon.
J’ai maintenant une fonction d’assertion et une page de tests unitaires facilement lisible pour ma fonction de conversion.
 
Développons un peu cette fonction de conversion et faisons en sorte qu’elle puisse convertir notre euro en plusieurs monnaies différentes en ajoutant un paramètre de devise en entrée :
( euroToDollar.js devient convertEuro.js et euroToDollar-test.js devient convertEuro-test.js

function convertEuro(euro, currency){
 switch(currency){
  case 'USD' :
   return euro * 1.3; 
  case 'GBP' :
   return euro * 0.87;
  case 'JPY' :
   return euro * 124.77;
  default : {
   throw new Error('Currency not handled');
  }
 }
}

J’ai maintenant plus de cas à gérer dans mes tests et j’aimerais pouvoir séparer nos tests en fonction de la devise testée. C’est ici que le concept de “suite de tests” entre en jeu. Il me permet de créer une suite de tests reposant sur une logique du code.
 
J’ajoute la fonction de suite de tests :

function testcase(message, tests){
 var total = 0;
 var succeed = 0;
 var p = document.createElement('p');
 p.innerHTML = message;
 document.body.appendChild(p);
 for(test in tests){
  total++;
  try{
   tests[test]();
   succeed++;
  }catch(err){  
  }
 }
 var p = document.createElement('p');
 p.innerHTML = 'succeed tests ' + succeed + '/' + total ;
 document.body.appendChild(p);
}

Et voici mes suites de tests pour nos différentes devises :

testcase('I convert euro to usd', {
 'I test with one euro' : function(){
  assert('1€ should return 1,3$', convertEuro(1, 'USD') === 1.3);
 },
 'I test with two euros' : function(){
  assert('2€ should return 2,6$', convertEuro(2, 'USD') === 2.6);  
 }
})

testcase('i convert euro to gbp', {
 'I test with one euro' : function(){
  assert('1€ should return 0,87£', convertEuro(1, 'GBP') === 0.87);
 },
 'I test with two euros' : function(){
  assert('2€ should return 1,74£', convertEuro(2, 'GBP') === 1.74);
 }
})

testcase('i convert euro to jpy', {
 'I test with one euro' : function(){
  assert('1€ should return 124,77¥', convertEuro(1, 'JPY') === 124.77);
 },
 'I test with two euros' : function(){
  assert('2€ should return 249,56¥', convertEuro(2, 'JPY') === 249.56);
 }
})

testcase('I try with currency not handled by the function', {
 'I try with NZD' : function() {
  var messsage;
  try{
   convertEuro(1, 'NZD')
  } catch(err){
   message = err;
  }
  assert('convert euro to nzd should throw error', message === 'Currency not handled');
 }
})

Mes tests sont donc bien séparés par unité logique.
J’affiche pour chaque suite de tests le nombre de tests réussis sur le total de tests passés.


 
Une dernière chose, j’aimerai pouvoir définir la devise au début de chaque suite de tests et l’utiliser comme variable afin de pouvoir la changer facilement et éviter des erreurs de saisie.
Je fais appel ici au concept de setup et de teardown.

function testcase(message, tests){
 var total = 0;
 var succeed = 0;
 var hasSetup = typeof tests.setUp === 'function';
 var hasTeardown = typeof tests.tearDown === 'function';
 var p = document.createElement('p');
 p.innerHTML = message;
 document.body.appendChild(p);
 for(test in tests){
  if(test !== 'setUp' && test !== 'tearDown'){
   total++;
  }
  try{
   if(hasSetup){
    tests.setUp();
   }
   tests[test]();
   if(test !== 'setUp' && test !== 'tearDown'){
    succeed++;
   }
   if(hasTeardown){
    tests.tearDown();
   }
  }catch(err){  
  }
 }
 var p = document.createElement('p');
 p.innerHTML = 'succeed tests ' + succeed + '/' + total ;
 document.body.appendChild(p);
}

testcase('I convert euro to usd', {
 'setUp' : function(){
  this.currency = 'USD';
 },
 'I test with one euro' : function(){
  assert('1€ should return 1,3$', convertEuro(1, this.currency) == 1.3);
 },
 'I test with two euros' : function(){
  assert('2€ should return 2,6$', convertEuro(2, this.currency) == 2.6);  
 }
})
...

La fonction setUp sera appelée avant chaque test et la fonction tearDown après chaque test.
 
J’ai maintenant une suite de tests unitaires automatisés qui me permet de valider réellement le bon fonctionnement de ma fonction de conversion de devises. Ces tests me permettront de vérifier la non régression de ma fonction lors de refactoring ou d’ajout de nouvelles fonctionnalités et de tester celles-ci facilement sur différentes plateformes (firefox, chrome, IE…).

Et avec un framework ?

Nous venons de voir une solution "maison" de la mise en place de tests unitaires. Cette solution suffit à des projets simples mais peut vite montrer ses limites sur des projets de plus grande envergure. Heureusement, des frameworks de tests javascrit sont là pour nous donner les outils nécessaires à nos besoins.
Je vais maintenant tester ma fonction convertToEuro avec le framework QUnit, en écrivant les mêmes tests que ma solution maison.

Après avoir téléchargé les sources sur le site, je crée la page html de présentation des résultats :

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>convertToEuro tests with QUnit</title>
    <link rel="stylesheet" href="qunit.css">
</head>
<body>
 <div id="qunit"></div>
 <div id="qunit-fixture"></div>
 <script src="qunit.js"></script>
 <script src="../convertEuro.js"></script>
 <script src="converEuro-test-QUnit.js"></script>
</body>
</html>

Voici la page des tests :

module('I convert euro to usd',{
    setup: function() {
        this.currency = 'USD';
    }
});
test('1€ should return 1,3', function() {
    strictEqual(convertEuro(1, this.currency), 1.3, 'succeed !');
});
test('1€ should return 2,6', function() {
    strictEqual(convertEuro(2, this.currency), 2.6, 'succeed !');
});

module('I convert euro to gbp',{
    setup: function() {
        this.currency = 'GBP';
    }
});
test('1€ should return 0,87£', function() {
    strictEqual(convertEuro(1, this.currency), 0.87, 'succeed !');
});
test('2€ should return 1,74£', function() {
    strictEqual(convertEuro(2, this.currency), 1.74, 'succeed !');
});

module('I convert euro to jpy',{
    setup: function() {
        this.currency = 'JPY';
    }
});
test('1€ should return 124,77¥', function() {
    strictEqual(convertEuro(1, this.currency), 124.77, 'succeed !');
});
test('2€ should return 249,56¥', function() {
    strictEqual(convertEuro(2, this.currency), 249.56, 'succeed !');
});

module('I try with currency not handled by the function');
test('I try with NZD', function(){
    throws(
        function() {
            convertEuro(1, 'NZD');
        },
        /Currency not handled/,
        "throws error Currency not handled"
    );
});

Et voici le résultat :

On peut remarquer que la logique des tests est pratiquement la même. Nos testcase sont appelés ici des module et nos assert maisons sont remplacés par l’assertor de QUnit strictEqual.
QUnit a aussi les notions de setup et teardown qui sont définis dans chaque module de tests.

Tout en gardant la même logique, QUnit introduit de nouveaux outils tel que la vérification des erreurs grâce à son throws utilisé ici pour valider que notre fonction ne prend pas en compte la devise NZD.

QUnit fournit aussi des outils de callbacks, de configuration de l’environnement de tests ainsi que la possibilité de tester des fonctions asynchrones. Il existe aussi de nombreux plugins permettant de rajouter des logs, de mocker des appels ajax etc.

QUnit fournit aussi une interface d’affichage de résultats détaillés. On peut voir la version du navigateur utilisée, le nombre de tests lancés, réussis et échoués. Une chose très appréciable est que QUnit nous donne des informations sur les raisons d’échec d’un test. Concernant le test 6, on peut rapidement voir quel était le résultat attendu et celui retourné. Efficace.

Vous pouvez retrouver les sources de cet article sur ce github.

Et ensuite ?

Nous voyons qu’il est très facile d’implémenter des tests unitaires pour nos fonctions en javascript. L’exemple montré dans cet article est très basique et les besoins sont souvent plus complexes. Les frameworks javascript de tests reposent la plupart du temps sur la logique présentée dans cet article et mettent à disposition de nombreux outils tels que des spies, des stubs ou des mocks pour répondre aux besoins d’applications plus complexes. Nous découvrirons tous ces outils lors d’un prochain article et nous verrons toute la puissance que peut nous offrir ces frameworks pour garantir le bon fonctionnement de nos applications javascript !

Publié par

Commentaire

2 réponses pour " Introduction aux tests unitaires en javascript "

  1. Publié par , Il y a 6 années

    Sympa l’article.
    QUnit n’est pas le meilleur des frameworks. Tu devrais regarder Mocha ou encore Jasmine.

  2. Publié par , Il y a 6 années

    Merci !
    Ce n’est sans doute pas le meilleur mais il est très simple à mettre en place et à prendre en main.
    J’ai personnellement une préférence pour Jasmine et son côté BDD.
    J’ai d’ailleurs mis sur github un dossier avec les mêmes tests réalisés avec Jasmine.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.