Créer des tests unitaires avec Angular

Je suppose que comme tout le monde, vous faites des tests unitaires de votre application... Non ? ;-)

Bref, nous allons voir ici comment créer des tests unitaires simples avec Angular/AngularCLI/Karma/Jasmine.

Quand on crée une application avec Angular CLI, la bonne nouvelle, c'est qu'il met en place tout le paramétrage pour tester votre application. Vous pouvez le voir dans le fichier package.json ou il vous ajoute les références aux packages NPM pour karma, jasmine et tout un tas d'autres trrucs du même genre :

    "jasmine-core": "~2.99.1",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~1.7.1",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "~2.0.0",
    "karma-jasmine": "~1.1.1",
    "karma-jasmine-html-reporter": "^0.2.2",

Ainsi que les types pour bénéficier de l'intellisense :

    "@types/jasmine": "~2.8.6",
    "@types/jasminewd2": "~2.0.3",

Karma  est le moteur qui permet de lancer les tests dans un vrai navigateur (ici, Chrome, cf karma-chrome-launcher), le test runner (développé par l'équipe d'AngularJS). Jasmine est lui le framework de test à proprement parlé.

Vos tests unitaires (et vos tests d'intégration), vous devrez les écrire dans un fichier dont le nom devra se terminer par .spec.ts. Dès que le moteur détecte un fichier de ce genre, il l'intègre dans l'ensemble des tests.

Vos tests seront exécutés dans le navigateur (et pas une émulation quelconque) afin de s'assurer que dans la vraie vie, vos tests correspondront bien à ce que l'on attend.

Pour commencer, créons une nouvelle application avec Angular CLI :

ng new myApp

Puis, pour simplifier, supprimez le fichier app.component.spec.ts (on verra cela un peu plus tard).

Enfin, créez un nouveau service super compliqué, CalculatorService :

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class CalculatorService {

  constructor() { }

  add(a: number, b: number) {
    return a + b;
  }

  lowerOrZero(numbers: number[]) {
    if (numbers == null) { return 0; }
    if (numbers.length === 0) { return 0; }

    const result = Math.min(...numbers);
    return result < 0 ? 0 : result;
  }
}

C'est le service de la mort qui tue !

Notre premier test unitaire

Ajoutons un fichier de test nommé calculator.service.spec.ts. Tout test unitaire se décompose en trois actes AAA : Arrange, Act, Assert.

Dans Arrange, nous préparons notre test.

Dans Act nos exécutons le test.

Dans Assert, nous vérifions les résultats du test.

Dans jasmine, cela se traduit par un code qui a la structure suivante :

describe('Intitulé du test', () => {

  it('Calcul compliqué!', () => {
    // Arrange
    let a = 4;

    // Act
    let result = a * 2;

    // Assert
    expect(result).toBe(8);
  });
});

describe permet de regrouper les tests d'une même nature qui contient chaque it, chaque test unitaire. Ici, dans l'unique test unitaire intitulé 'Calcul compliqué', on définit les paramètres initiaux (a=4), on effectue le test (result = a*2) et on test le résultat avec expect(result).toBe(8). Naturel, intuitif, simple.

Pour exécuter notre test, Angular CLI nous a ajouté la commande qu'il faut, il suffit dans la console de taper :

npm test

A ce moment, il compile votre code et lance le navigateur puis exécute les tests et vous montre le résultat dans le navigateur :

Et dans la console, vous voyez plus d'informations (utilse en cas de problème).

Surtout ne fermez pas le navigateur : retournez dans votre code, faites une petite modification et sauvegardez : aussitôt, votre code est recompilé et les tests réexécutés. Cool en cas de problème.

Testons maintenant le service. On pourrait écrire :

describe('CalculatorService simple test', () => {
  describe('add', () => {
    const service = new CalculatorService();
    it('should return correct answer', () => {
      const result = service.add(4, 2);

      expect(result).toBe(6);
    });
  });
});

Mais si l'on veut écrire un autre test unitaire (it) avec de nouveau un service, il faut de nouveau l'instancier (ici c'est juste un new CalculatorService() mais on peut avoir des paramètres initiaux plus complexe. On pourrait mettre l'instanciation du service avant la première méthode it mais dans ce cas, ce serait toujours la même instance du service qui serait utilisé par tous les tests it ce qui est mal ;-). Heureusement, jasmine propse une méthode beforeEach qui sera utilisée à chaque exécution d'un it. Donc notre code sera plutôt le suivant (j'ai rajouté un deuxième test) :

import { CalculatorService } from './calculator.service';

describe('CalculatorService simple test', () => {
  let service: CalculatorService;

  beforeEach(() => {
    service = new CalculatorService();
    console.log('creation...');
  });

  it('add: should return correct answer', () => {
    const result = service.add(4, 2);

    expect(result).toBe(6);
  });

  it('lowerOrZero: Should answer correct answer', () => {
    const result = service.lowerOrZero([2, 1, 3]);
    expect(result).toBe(1);
  });
});

Pour mieux organiser encore ses tests, sachez qu'un describe peut contenir un autre describe. Dans le code suivant, j'ai regroupé mes tests par méthode du service testé. Le résultat est beaucoup plus lisible :

import { CalculatorService } from './calculator.service';

describe('CalculatorService simple test', () => {
  let service: CalculatorService;

  beforeEach(() => {
    service = new CalculatorService();
    console.log('creation...');
  });

  // tests de add
  describe('add', () => {
    it('should return correct answer', () => {
      const result = service.add(4, 2);

      expect(result).toBe(6);
    });
  });

  // tests de lowerOrZero
  describe('lowerOrZero', () => {
    it('Should answer 0 for null', () => {
      const result = service.lowerOrZero(null);
      expect(result).toBe(0);
    });

    it('Should answer correct answer', () => {
      const result = service.lowerOrZero([2, 1, 3]);
      expect(result).toBe(1);
    });

    it('Should answer 0 if negative', () => {
      const result = service.lowerOrZero([2, 1, -3]);
      expect(result).toBe(0);
    });
  });
});

Remarquez dans la console que l'on a bien crée 4 fois le service.

Ici nous avons utilisé le Matcher toBe mais jasmine en propose tout une ribambelle et vous pouvez même créer vos propres matchers :

  • toBe,
  • toBeFalsy, toBeThrusly,
  • toBeGreaterThan, toBeLesserThan, etc.
  • toContain,
  • toHaveBeenCalled,
  • toHaveBeenCalledWith,
  • etc, etc.

Et après ?

Ici, nous avons réalisé des tests très simples mais on peut se poser la question : quid si le service utilise d'autres services ? Peut on 'mocker' facilement ces services ? Comment faire ? Quid des templates html d'Angular ?

Nous verrons cela dans un prochain épisode.

Suspense...

blog comments powered by Disqus