Tests Javascript avec Jasmine - Partie 1 : tester le code oublié

Publié par Jean-Nicolas Viens le jeudi 30 mai 2013 à 15:50

De nos jours, la tendance du web est au web interactif ou 2.0, voir 3.0, au SPA (Single Page Application), aux clients riches et autres “buzzwords” de cette catégorie. Comment répondre à cette demande? En augmentant l’interaction avec l’usager à même le navigateur. Par exemple, on voudra potentiellement ne recharger qu’une seule partie d’une page, ou encore valider un formulaire du côté client. Pour introduire ce nouveau paradigme, on déplace du code, de la logique, du côté client à l’aide du Javascript. Mais cette logique qui était, je l’espère, testée sur le serveur se retrouve probablement pêle-mêle, non testée et non testable. Ça brise tout le temps, le navigateur a un comportement inexplicable, et on perd un contrat, oops! Comment s’assurer que cela n’arrive pas ? Ce premier article d’une série de 3 explore les possibilités de la librairie d'essais unitaires Jasmine.js.

Tests Javascript, version BDD

Il est souvent difficile de tester du Javascript unitairement, mais il existe un outil, selon moi, très efficace pour ces cas : le français. Quoi de plus simple à interpréter comme résultat d’un test :

La librairie de conversion
   Conversion de livre à kilo
     Devrait multiplier le nombre de livres par 2.2
     Devrait lancer une exception si l’entrée n’est pas numérique

On voit clairement que la deuxième fonctionnalité n’est pas respectée. C’est bien, mais ce n’est pas tout! Le résultat du test est simple à lire, mais le test lui-même l’est tout autant :

describe("La librairie de conversion", function() {
  describe("Conversion de livre à kilo", function() {

    it("Devrait multiplier le nombre de livre par 2.2", function() {
      expect(Conversion.livreVersKilo(22)).toBe(10);
    });

    it("Devrait lancer une exception si l'entrée n'est pas numérique", function()  {
             expect(Conversion.livreVersKilo.bind("abc"))
               .toThrow("ValeurLivreIncorrecte");
    });

  });
});

Note : dans le deuxième test, on utilise la fonction .bind("abc), ce qui n’est pas nécessairement intuitif. Voir l’explication ci-bas

Et c’est aussi simple que cela. Ou presque. Il y a quelques notions technique à connaître pour que tout fonctionne, mais la plus grosse partie du travail est terminée : le comportement voulu est décrit en Javascript, tout en étant simple à lire. Il est facile de comprendre que la partie “conversion de livre à kilo” de la “librairie de conversion” devrait exposer ce comportement. On est donc en mesure de s’attendre à ce que :

Conversion.livreVersKilo(22); // -> 10
Conversion.livreVersKilo("abc"); // -> exception

Et c’est ici que Jasmine entre en jeu. Il s’agit d’un “framework” de test Javascript extrêmement puissant et élégant à utiliser. En voici les grandes lignes, mais je vous encourage à en lire davantage ici.

Note : cherchez “jasmine js” et non “jasmine” sur google, les résultats seront plus simples à justifier à votre patron !

À la rencontre de Jasmine

Jasmine offre plusieurs fonctionalités permettant de convertir le français en tests exécutables, mais tout d’abord voyons à quoi ressemble un test Jasmine. Une suite de tests débute par un appel à la fonction describe() qui prend 2 arguments : le comportement défini (string) et une fonction (généralement une fonction anonyme). Ensuite, il s’agit d’utiliser la fonction it() pour chaque validation du comportement et celle-ci prend également une string et une fonction comme paramètres. Dans l’exemple ci-haut, les 2 premières lignes sont des describe() et les 2 suivantes sont des it().

Voici un exemple complet; n’oubliez pas de regarder l’onglet résultat (Result) :

Utilisation de Jasmine

Tout d’abord, introduisons quelques mots de vocabulaire pour notre contexte :

  1. Runner : Un “runner” ou “test runner” est responsable de trouver et d’exécuter les tests. Par exemple, certains tests runner offriront la possibilité de classer des tests par catégorie pour n’en exécuter qu’une seule. D’autres offriront de la paraléllisation.
  2. Reporter : Un “reporter” s’occupe de relayer ou d’afficher les résultats d’une suite de tests.

Par défaut, Jasmine utilise un runner qui roule tous les tests découverts et un reporter qui affiche les résultats en HTML. Il existe d’autres reporters, par exemple celui de ReSharper qui communique les résultats à la fenêtre de session de tests dans Visual Studio. Chutzpah incorpore également sa version d’un “reporter” Jasmine.

Dans un prochain article, nous décrirons comment intégrer des tests Jasmine à son environnement de travail et à l’intégration continue. Pour l’instant, nous nous contenterons de l’exemple complet à la fin de cet article.

Les assertions

Jasmine.js nous rend la vie facile avec sa syntaxe fluente :

var vehicule = new Automobile();
expect(vehicule.nombrePortes).toBe(4);

Pour en revenir au sujet principal, débutons par regarder la façon de valider un résultat dans notre test. Jasmine utilise une syntaxe fluente pour ses assertions à l’aide de la fonction expect(<valeur>). Comme on peut le voir dans le test ci-haut, la syntaxe donne un résultat facile à lire, un peu à la Fluent Assertions pour .NET. Pour résumé, on utilise :

expect(valeur).to{NomExpectation}(valeur_attendue);

Vocabulaire : La partie to[NomExpectation] (par exemple, toBeGreaterThan()) est appelée un “matcher”

Note : selon le “matcher” utilisé le paramètre valeur_attendue n’est pas toujours présent.

On peut également inverser la condition avec le mot-clé “not” :

expect(valeur).not.to{NomExpectation}(valeur_voulue);

Vous trouverez ici une liste des “matchers” fournis avec Jasmine. Ceux-ci ne sont pas aussi exhaustifs que dans d’autres “frameworks”, principalement dû au fait qu’il est extrêmement simple d’en ajouter. Ce sujet sort de la portée de cet article, mais vous trouverez sur la même page une procédure détaillée et simple à suivre pour y arriver.

Deux de ces matchers méritent peut-être une explication supplémentaire : toBeTruthy() et toBeFalsy(). Pourquoi ne pas avoir utilisé toBeTrue() et toBeFalse()? Et bien, simplement parce que ces “matchers” valident plus que la simple valeur true/false. Ils valident la véracité (ou non) d’une expression, tout comme une condition if() en Javascript. Par exemple, une valeur de 0 est considérée comme “falsy”. Si seule la valeur true ou false doit être considérée, utilisez plutôt la fonction toBe() :

expect(valeur).toBe({true|false});

Voici quelques exemples de ces 2 “matchers” en action :

Un autre cas plus particulier est celui où l’on veut que notre fonction lance une exception. Supposons que la fonction test() lance une exception, on ne peut pas faire ceci :

expect(test()).toThrow();

En appelant ainsi la fonction test(), on lance directement l’exception, alors .toThrow() ne sera jamais appelé et le test échouera. Pour corriger la situation, on passe simplement la fonction à expect() et .toThrow()l’invoquera pour nous, comme ceci :

expect(test).toThrow();

Le seul hic : comment passer des paramètres? On pourrait utiliser une fonction anonyme, mais il est beaucoup plus simple d’utiliser la fonction .bind(), qui associera les paramètres à notre fonction, sans l’invoquer.

De la théorie à la pratique

Voici un exemple complet qui intègre tout ce qui a été vu à date en se basant sur le premier exemple. Cet exemple couvre tous les points vu jusqu’ici :

  • Structure d’un test
  • Assertions
  • Runner
  • Reporter

Je vous propose la disposition suivante pour les fichiers, mais ce n’est pas une obligation. Par exemple, en ASP.NET MVC 4, on a le dossier ~/Scripts, alors nous nous sommes plutôt créer un dossier ~/Scripts.Tests avec les tests jasmine. Voici à quoi ressemble cette structure :

Structure des fichiers Jasmine

Nous avons donc :

  1. Un dossier specs où seront placés les tests
  2. Un dossier src où se trouve notre code source
  3. Un dossier specs/vendor contenant les fichiers de parties tiers (ici il contient Jasmine)
  4. Un dossier specs/integration qui contient les tests d’intégration (ce fichier est vide pour l’instant)
  5. Un dossier specs/src qui contient les tests unitaires associés aux fichiers dans src, avec l’extension .spec.js (.test.js est souvent utilisé aussi)
  6. Un fichier index.html avec le runner et reporter. C’est lui qui sert à l’exécution de notre suite de test.

Voici maintenant le contenu de ces fichiers :

specs/index.html

<html>
<head>
  <script type="text/javascript" src="jasmine/jasmine.js"></script>
  <script type="text/javascript" src="conversion.js"></script>
  <script type="text/javascript" src="conversion.specs.js"></script>
  <script type="text/javascript">
    (function () {
      var jasmineEnv = jasmine.getEnv();
      jasmineEnv.updateInterval = 250

      var htmlReporter = new jasmine.HtmlReporter();
      jasmineEnv.addReporter(htmlReporter);
      jasmineEnv.specFilter = function (spec) {
        return htmlReporter.specFilter(spec);
      };

      function execJasmine() {
        jasmineEnv.execute();
      }

      var currentWindowOnload = window.onload;
      window.onload = function () {
        if (currentWindowOnload) {
          currentWindowOnload();
        }

        document.querySelector('.version').innerHTML = jasmineEnv.versionString();
        execJasmine();
    };

  })();
</script>
</head>
<body></body>
</html>

src/conversions.js

var Conversion = {
  livreVersKilo: function(livres) {
    if(isNaN(livres - 1)) {
      throw "ValeurLivreIncorrecte";
    }
    return livres / 2.2;
  }
};

specs/conversions.spec.js

describe("La librairie de conversion", function() {
  describe("Conversion de livre à kilo", function() {

    it("Devrait multiplier le nombre de livre par 2.2", function() {
      expect(Conversion.livreVersKilo(22)).toBe(10);
    });

    it("Devrait lancer une exception si l'entrée n'est pas numérique", function()  {
             expect(Conversion.livreVersKilo.bind("abc"))
               .toThrow("ValeurLivreIncorrecte");
    });

  });
});

Conclusion

Bien que jasmine.js nous permet déjà de tester la majorité de notre code, on ne s’arrêtera pas là! Jasmine.js est doté de plusieurs autres outils facilitant la vie des développeurs Javascript. Notamment, nous verrons comment utiliser des mocks pour isoler la fonctionnalité testée et aussi comment contrôler le temps!

Lire le billet suivant

blog comments powered by Disqus

0 Comments:

Post a comment

Comments have been closed for this post.