Jasmine pour des tests en JavaScript

Jasmine flowers
Archia Oryix

Les tests unitaires sont des méthodes permettant de tester de façon unitaire des éléments d’un code source. Leur principal objectif est de vérifier le bon fonctionnement de sous-ensembles de code tels que les fonctions, les procédures, les classes, etc.. Les tests unitaires garantissent que ces éléments de base fonctionnent comme souhaité et préviennent des potentiels bugs.

Jasmine est un framework de tests open-source pour JavaScript. Il possède une syntaxe assez facile à prendre en main, finalement assez proche de RSpec pour Ruby, un framework dit Behaviour-Driven Development.

Appréhender la syntaxe de Jasmine

Une suite de tests Jasmine débute avec un appel à describe, une fonction globale de Jasmine qui prend en considération deux paramètres : une chaîne de caractères et une fonction. La chaîne de caractères représente un titre pour la suite, rappelant généralement ce qui va être testé. La fonction, quant à elle, est un bloc de code qui implémente la suite.

Les spécifications, appelées simplement specs, sont définies par l’appel de la fonction globale it de Jasmine, qui elle aussi prend en paramètre une chaîne de caractères et une fonction. La chaîne de caractères définit le test et la fonction est, à proprement parler, le test. Une spec peut contenir une à plusieurs attentes – expectations. En Jasmine, une expectation est une assertion pouvant être vraie ou fausse. Un test qui passe est un test possédant toutes ses expectations à true. Un test qui échoue possède au moins une expectation à false.

describe("My first suite", function() {
  it("contains spec with an expectation", function() {
    expect(true).toBe(true);
  });
});

Les blocs describe et it sont des fonctions. Celles-ci peuvent donc contenir tout le code nécessaire à l’exécution des tests. Les règles classiques de scope en JavaScript s’y appliquent ; ce qui signifie que toutes les variables déclarées dans un bloc describe seront disponibles dans chacun des blocs it que possède la suite.

describe("My first suite", function() {
  var a = true;

  it("contains spec with an expectation", function() {
    expect(a).toBe(true);
  });
});

Les expectations sont construites à partir de la fonction expect qui prend en argument une valeur, appelée la valeur réelle. Le tout est suivi d’une fonction dite matcher qui prend en argument la valeur attendue.

Before et After

Il est possible d’exécuter du code avant ou après chacune des specs écrites, respectivement grâce aux fonctionnalités beforeEach et afterEach. Cela peut devenir très pratique si l’on veut factoriser du code, ou si l’on utilise des variables globales que l’on soit réinitialiser après un test.

describe("My first suite with 'beforeEach' and 'afterEach'", function() {
  var a = 0;

  beforeEach(function() {
    a += 1;
  });

  afterEach(function() {
    a = 0;
  });

  it("checks the value of a", function() {
    expect(a).toEqual(1);
    a += 1;
  });

  it("expects a to still be equal to 1", function() {
    expect(a).toEqual(1);
  });
});

Il est également possible d’exécuter du code avant ou après toutes les specs contenues dans une suite. Comme le suggère son nom, la fonction beforeAll est appelée avant l’exécution de toutes les specs, et afterAll est appelée après l’exécution de toutes les specs.

describe("My first suite with 'beforeAll' and 'afterAll'", function() {
  var a;

  beforeAll(function() {
    a += 1;
  });

  afterAll(function() {
    a = 0;
  });

  it("checks the value of a", function() {
    expect(a).toEqual(1);
    a += 1;
  });

  it("does not reset a between specs", function() {
    expect(a).toEqual(2);
  });
});

Les matchers

Un matcher produit une comparaison booléenne entre la valeur réelle d’un élément et sa valeur attendue. Si ces deux valeurs sont divergentes, l’expectation est considérée comme fausse, et Jasmine fait échouer la spec.

Un matcher peut aussi être évalué avec une assertion négative. Il suffit pour cela d’ajouter le mot clé not avant l’utilisation du matcher.

describe("My first suite", function() {
  var a = true;

  it("contains spec with an expectation", function() {
    expect(a).not.toBe(false);
  });
});

Un grand nombre de matchers sont disponibles nativement dans Jasmine.

toBe

Le matcher toBe compare deux valeurs entre elles grâce à l’opérateur === en JavaScript.

describe("The 'toBe' matcher", function() {
  var a = 1,
      b = 2;

  it("compares with ===", function() {
    expect(a + b).toBe(3);
    expect(a).not.toBe(b);
  });
});

toEqual

Le matcher toEqual permet de vérifier l’équivalence de deux valeurs. toBe est donc plus strict que toEqual.

describe("The 'toEqual' matcher", function() {
  it("works with simple variables", function() {
    var a = 4;
    expect(a).toEqual(4);
  });

  it("works also well with arrays", function() {
    var b = new Array('1', '2'),
        c = new Array('1', '2');
    expect(b).toEqual(c);
  });
});

toMatch

Le matcher toMatch permet la comparaison avec les expressions régulières. Il permet de tester la bonne structure d’une valeur selon un pattern bien défini.

toMatch fonctionne également avec les chaînes de caractères, qui seront ensuite analysées comme des expressions régulières.

describe("The 'toMatch' matcher", function() {
  var message = "hello world!";

  it("compares with RegExp", function() {
    expect(message).toMatch(/^hello/);
    expect(message).toMatch("hello");
    expect(message).toMatch(/world!$/);
    expect(message).not.toMatch(/boy/);
  });
});

toBeNull

toBeNull permet de tester la nullité d’un élément. Si cet élément possède une valeur dite null, alors l’expectation sera considérée true. Le cas échéant, Jasmine fait échouer la spec.

describe("The 'toBeNull' matcher", function() {
  var a = null,
      b = "test";

  it("compares with null", function() {
    expect(a).toBeNull();
    expect(b).not.toBeNull();
  });
});

toContain

toContain permet de vérifier la présence d’un élément dans un tableau.

describe("The 'toContain' matcher", function() {
  var a = ["banana", "pineapple", "coconut"];

  it("finds an item in an Array", function() {
    expect(a).toContain("banana");
    expect(a).not.toContain("strawberry");
  });
});

toContain fonctionne également avec les chaînes de caractères : la fonction permet alors de vérifier si un ensemble de caractères figurent dans la chaîne globale.

describe("The 'toContain' matcher", function() {
  var a = "The quick brown fox jumps over the lazy dog";

  it("finds a substring in an String", function() {
    expect(a).toContain("fox");
    expect(a).not.toContain("foxy");
  });
});

toBeTruthy et toBeFalsy

Les matchers toBeTruthy et toBeFalsy permettent de tester de manière booléenne des valeurs. En JavaScript, tous types de valeurs peuvent être utilisés pour obtenir une valeur booléenne, par exemple lors de l’utilisation de la déclaration conditionnelle if.

Les valeurs suivantes seront interprétées comme étant false : undefined, null, false, 0, NaN et la chaîne de caractères vide "". Toutes les autres valeurs – objets et tableaux compris – sont considérées comme true. Les valeurs interprétées comme false sont appelées falsy, et les valeurs interprétées comme true sont appelées truthy.

Boolean(), appelé en tant que fonction, convertit son paramètre en booléen. Vous pouvez utiliser cette fonction pour savoir comment une valeur sera interprétée.

describe("The 'toBeTruthy' and 'toBeFalsy' matchers", function() {
  var a,
      b = false,
      c = 15;

  it("is for boolean casting testing", function() {
    expect(a).toBeFalsy();
    expect(a).not.toBeTruthy();
  });

  it("is for boolean casting testing", function() {
    expect(b).toBeFalsy();
    expect(b).not.toBeTruthy();
  });

  it("is for boolean casting testing", function() {
    expect(c).toBeTruthy();
    expect(c).not.toBeFalsy();
  });
});

toBeDefined et toBeUndefined

toBeDefined et toBeUndefined permettent de vérifier l’existence ou non d’une valeur. À noter une nouvelle fois que la valeur de type undefined est aussi considérée comme étant false.

En JavaScript, les variables non-initialisées ne possèdent pas de valeur, elles sont undefined. Une propriété inexistante pour un objet renvoie également une valeur dite undefined. Une fonction qui ne possède pas de return, ou une fonction possédant un return vide, est une fonction qui retourne undefined.

describe("The 'toBeDefined' and 'toBeUndefined' matchers", function() {
  var a,
      b = 3,
      c = (function() {})();

  it("compares against `defined`", function() {
    expect(a).not.toBeDefined();
    expect(b).toBeDefined();
    expect(c).not.toBeDefined();
  });

  it("compares against `undefined`", function() {
    expect(a).toBeUndefined();
    expect(b).not.toBeUndefined();
    expect(c).toBeUndefined();
  });
});

toBeLessThan et toBeGreaterThan

toBeGreaterThan et toBeLessThan permettent de vérifier si un élément est plus grand, ou s’il est plus petit, qu’un autre.

describe("The 'toBeLessThan' and 'toBeGreaterThan' matchers", function() {
  var a = 1,
      b = 2,
      c = 3;

  it("is for mathematical comparisons", function() {
    expect(a).toBeLessThan(b);
    expect(b).not.toBeGreaterThan(c);
    expect(c).not.toBeLessThan(a);
  });
});

toBeCloseTo

toBeCloseTo permet de vérifier si un nombre est proche d’un nombre, selon un certain nombre de décimales, donné en deuxième argument du matcher.

describe("The 'toBeCloseTo' matcher", function() {
  var a = 1.2,
      b = 1.25;

  it("is for precision math comparison", function() {
    expect(b).toBeCloseTo(a, 1);
    expect(b).not.toBeCloseTo(a, 2);
  });
});

En utilisant deux décimales dans l’exemple ci-dessous, toBeCloseTo considère que le deuxième chiffre décimal diffère pour les deux nombres choisis. Mettre le deuxième argument à 0 arrondit les nombres aux entiers.

toBeCloseTo est assez difficile à prendre en main. Je vous recommande de bien tester son comportement avant de le mettre en œuvre dans plusieurs de vos tests.

toThrow et toThrowError

toThrow permet de tester les erreurs. En utilisant ce matcher, Jasmine s’attend à recevoir une erreur, et c’est donc seulement en présence de celle-ci que la spec passera.

toThrowError permet de tester spécifiquement des erreurs. Ce matcher autorise en argument une expression régulière, une chaîne de caractères ou un type d’erreurs.

describe("The 'toThrow' matcher", function() {
  var f = function(){ throw new Error(); },
      g = function(){ throw new TypeError("I failed!"); };

  it("tests if a function throws an exception", function() {
    expect(f).toThrow();
  });

  it("tests a specific thrown exception", function() {
    expect(g).toThrowError(/fail/);
    expect(g).toThrowError("I");
    expect(g).toThrowError(TypeError);
  });
});

Des matchers personnalisés

Il est également possible de créer ses propres matchers. Il faut ajouter son matcher au sein du fichier contenant les specs qui doivent l’utiliser. Pour cela, il est possible d’utiliser beforeEach ou beforeAll.

Dans les versions de Jasmine supérieures à la 2.0, la fonction compare reçoit la valeur passée à la fonction expect comme étant son premier argument, la valeur réelle, et la valeur (si celle-ci existe) donnée au matcher comme étant son deuxième argument, la valeur attendue. Les matchers personnalisés prennent en argument deux paramètres : util, qui est un set de fonctions utiles pour les matchers, voir matchersUtil.js pour une liste détaillée ; et customEqualityTesters, qui a besoin d’être appelé si util.equals est utilisé.

Dans le cas présent ci-dessous, je crée un matcher ne prenant en considérant que la valeur réelle, actual, et je vérifie si celle-ci est paire grâce à util.equals. Cette fonction prend en paramètre la valeur réelle, la valeur attendue, et l’objet customEqualityTesters. La valeur réelle est ici, une opération avec modulo 2 qui consiste à vérifier si mon nombre est pair, et la valeur attendue est true. Si jamais cette opération donne un résultat égal à false, le matcher toBeEven me renvoie le message « Expected <number> to be even » et la spec échoue.

describe("The 'toBeEven' matcher", function() {
  beforeAll(function() {
    jasmine.addMatchers({
      toBeEven: function(util, customEqualityTesters) {
        return {
          compare: function(actual){
            return { pass: util.equals(actual%2==0, true, customEqualityTesters) }
          }
        }
        this.message = function() {
          return "Expected " + actual + " to be even";
        };
      }
    });
  });

  if('expects numbers to be even or not', function(){
    expect(2).toBeEven();
    expect(6857).not.toBeEven();
  });
});

Un peu de pratique est nécessaire pour bien comprendre comment mettre en place ses propres matchers. Mais vous verrez que les possibilités sont énormes, et que cela peut vous permettre d’avoir un code plus lisible si vos suites possèdent beaucoup de specs.

Écrire un premier test Jasmine

Ainsi, il est, par exemple, très simple de vérifier à l’aide de Jasmine, le bon comportement de la fonction du crible d’Eratosthenes présente ci-dessous. Cette fonction est censée retourner un tableau de tous les nombres premiers inférieurs à n.

function eratosthenes(n) {
  var detectprimes = new Array(n),
      primes = new Array();

  detectprimes[0] = false;
  detectprimes[1] = false;

  for(var i=2; i < detectprimes.length; i++)
    detectprimes[i] = true;

  for(var p=2; p < Math.sqrt(n); p++) {
    if(detectprimes[p]) {
      for(var j = p*p; j < n; j+= p)
        detectprimes[j] = false;
    }
  }

  for (var i = 0; i < n; i++)
    if(detectprimes[i]) primes.push(i);

  return primes;
}

Dans la suite créée, on souhaite obtenir un tableau de tous les nombres premiers en-dessous de 100. Pour cela, on utilise la fonction eratosthenes avec en paramètre le nombre 100.

Le test doit effectuer les vérifications suivantes. Ce tableau doit contenir 25 éléments. Parmi eux doivent figurer les nombres 5, 11, 43 et 97, choisis de façon totalement arbitraire. Mais les nombres 28, 60 et 99 ne doivent pas appartenir à ce tableau, car ils ne sont pas premiers.

describe('Prime numbers under 100 array', function(){
  var primes = eratosthenes(100);

  it('is truthy', function(){
    expect(primes).toBeTruthy();
  });

  it('is really an array', function(){
    expect(Array.isArray(primes)).toBe(true);
  });

  it('has 25 primes under 100', function(){
    expect(primes.length).toEqual(25);
  });

  it('contains 5, 11, 43, 97 as primes', function(){
    expect(primes).toContain(5);
    expect(primes).toContain(11);
    expect(primes).toContain(43);
    expect(primes).toContain(97);
  });

  it('does not contain 28, 60, 99', function(){
    expect(primes).not.toContain(28);
    expect(primes).not.toContain(60);
    expect(primes).not.toContain(99);
  });
});

Laisser un commentaire

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