Souvent, parfois, vous souhaiterez modifier des données affichées dans un modèle. Par exemple, vous souhaiterez peut-être formater un nombre en tant que devise, transformer une date dans un format plus facile à comprendre ou mettre du texte en majuscule. Dans des situations comme celles-ci, Angular fournit un moyen de transformer des données à l’aide de ce qu’on appelle un « pipe » (prononcez « paillepe »). Les « pipes » prennent une valeur en entrée, la transforment, puis renvoient la valeur transformée. Parce que le fonctionnement des « pipes » est simple, écrire des tests pour eux l’est aussi. Les « pipes » ne dépendent que de leur valeur d’entrée. Une fonction dont la sortie ne dépend que de l’entrée qui lui est transmise est aussi appelée fonction pure.

Lorsqu’une fonction peut faire autre chose que renvoyer une valeur, on dit qu’elle a un « side effect » (ou effet secondaire). Un « side effect » peut être la modification d’une variable globale ou l’exécution d’une requête HTTP. Les fonctions pures comme les « pipes » n’ont pas d’effets secondaires, c’est pourquoi elles sont faciles à tester.

Plan global du cours

Markup

Retrouvez le code de cet article sur ce dépôt stackblitz :

Open in StackBlitz

Ou suivez pas à pas les instructions 😇 !

Tester un pipe : PhoneNumber

Présentation du support des tests

Pour illustrer le propos de cet article, nous allons utiliser un « pipe » maison qui se propose de formater une chaîne de caractères en numéro de téléphone : PhoneNumberPipe. Tant que nous y sommes, faisons dans l’originalité 😇.

Ce « pipe » prend un numéro de téléphone sous la forme d’une chaîne (dans un format valide) et le transforme dans un format spécifié par l’utilisateur. Dans ce cas précis, les tests auront pour objectif de vérifier que la classe fonctionne comme attendu.

Du point de vue Angular, chaque « pipe » a une méthode nommée transform. Cette méthode est responsable du formatage de la valeur d’entrée du « pipe ». La signature de la fonction de transformation pour PhoneNumberPipe ressemble à ceci :

transform(value: string, countryCode: string = '33'): string {

value est transmis à la fonction à partir de la gauche du « pipe » et représente un numéro de téléphone. Comme ceci :

<p>{{ '0474769560' | phone }}</p>

countryCode est un paramètre facultatif qui ajoute un préfixe au numéro de téléphone en fonction du code international du pays. Par exemple, avec un code pays 33 (pour la France), le numéro de téléphone résultant serait +33 (XX) XX XX XX XX.

Pour rester simple, PhoneNumberPipe ne fonctionne qu’avec les numéros de téléphone qui suivent le plan de numérotation français. Il serait cependant assez simple d’améliorer cette classe qui se veut très simple pour les besoin de l’article. Jugez plutôt :

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'phoneNumber'
})
export class PhoneNumberPipe implements PipeTransform {

  transform(value: string, countryCode: string = '33'): string {
    if (!value) {
      return '';
    }

    const phoneNumber = value.toString().trim().replace(/[^0-9]/g, '');
    const formattedNumber = phoneNumber.replace(/^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/, `+${countryCode} ($1) $2 $3 $4 $5`);

    return formattedNumber;
  }
}

Snippet 7.1 – phone-number.ts

Tester PhoneNumber

Commençons par définir les différents scénarios que nous allons rencontrez avec cette classe, ou en anglais les « tests cases » :

ChaineAffichage
Une chaine de 10 caractères numériques affiche un numéro sous la forme ‘+33 (xx) xx xx xx xx’.« 0474769560 »+33 (04) 74 76 95 60
Les caractères non numériques sont ignorés.« 0474769p60 »47476960
Les caractères non numériques sont ignorés.« 0474769p560 »+33 (04) 74 76 95 60
Affiche la même chaine si le nombre de chiffres est inférieur à 10.« 474769560 »474769560
Tableau 7.1 – 3 tests cases à vérifier.

Comme pour les composants, les tests ont toujours la même structure :

// -- imports des dépendances -- //

// -- définition de la suite de tests -- //

Ce qui dans notre cas peut se matérialiser de la manière suivante :

import { PhoneNumberPipe } from './phone-number.pipe';  ①

describe('PhoneNumberPipe', () => {  ②
  let pipe: any;

  beforeAll(() => {
    pipe = new PhoneNumberPipe();  ③
  });

  describe('default behavior', ()=> {  ④

    it('should transform the string or number into the default phone format', () => {  ⑤
    });

  })

  afterAll(() => {
    pipe = null;  ⑥
  });
});
  • ① → import de l’unique dépendance, le « pipe » lui même.
  • ② → définition de la suite de tests.
  • ③ → définition du callback beforeEach : avant chaque test, un crée un nouvelle instance du « pipe ».
  • ④ → définition d’une sous-suite pour améliorer la consistance du test.
  • ⑤ → déclaration du premier test unitaire tiré du premier test case.
  • ⑥ → définition du callback afterEach : on s’assurance que la variable de test « pipe » ne possède plus de références.

Il ne reste plus qu’à compléter le test, qui d’après le tableau 7.1 nous dit : « Un chaine de 10 caractères numériques affiche un numéro sous la forme ‘+33 (xx) xx xx xx xx’ » :

it('should transform the string into the default phone format', () => {
  const inputPhoneNumber = '0987653498';
  const transformedPhoneNumber = pipe.transform(inputPhoneNumber);
  const expectedResult = '+33 (09) 87 65 34 98';
  expect(transformedPhoneNumber).toBe(expectedResult);
});

Je vous l’avez dit, tester des « pipes » est encore plus facile que les directives : pas besoin de toute l’artillerie du framework du tests d’Angular, ici nous testons de simples fonctions :

Figure 7.1 – Premier test ok.

Enchaînons par le second « test case », qui d’après le tableau 7.1 nous dit : « Les caractères non numériques sont ignorés. ». Ce « test case » présente en réalité 2 cas distincts :

  1. la chaine de caractère est composé de 10 chiffres et d’au moins un caractère non numérique.
  2. la chaine de caractère est composé de 9 chiffres ou moins et d’au moins un caractère non numérique.

Pourquoi faire cette distinction ? Après tout on pourrait simplement déclarer que si la chaine possède au moins un caractère non numérique, le « pipe » est inopérant. Mon avis sur la question est qu’une absence totale d’affichage ou aucun changement perceptible apporte beaucoup de confusion. On ne sait pas trop pourquoi ça ne fonctionne pas et on teste tout sortes de choses sans succès. De plus, simplement retirer les caractères non numériques et aboutir à une chaine de 10 caractères fait sens, c’est au final ce que l’on souhaite.

Maintenant, assez de bavardage et place au tests :

describe('letters behavior', () => {  ①

  it('should only remove non digit chars if there are less than 10 digits', () => {
    const inputPhoneNumber = '0474769p60';
    const transformedPhoneNumber = pipe.transform(inputPhoneNumber);
    const expectedResult = '047476960';
    expect(transformedPhoneNumber).toBe(expectedResult);
  });

  it('should remove non digit chars and format the string if there are at least 10 digits', () => {
    const inputPhoneNumber = '04747695p60';
    const transformedPhoneNumber = pipe.transform(inputPhoneNumber);
    const expectedResult = '+33 (04) 74 76 95 60';
    expect(transformedPhoneNumber).toBe(expectedResult);
  });

});

Ce deuxième « test case » peut être rassemblé dans une sous suite pour plus de lisibilité. C’est ce que j’ai fait en utilisant un bloc « describe » ( ① ) dans le le bloc principal du même nom.

Figure 7.2 – Le second test case passe.

En enfin, troisième test case qui d’après le tableau 7.1 nous dit : « Affiche la même chaine si le nombre de chiffres est inférieur à 10. ». On va se placer dans le sous suite « default behavior » (comportement pas défaut en français), car cette suite sous tend que la valeur à formatter et fournie au « pipe » ne contient que des caractères exploitables :

describe('default behavior', ()=> {

  it('should transform the string into the default phone format', () => {
    // --
  });

  it('should display the same string if there are less than 10 digits', () => {  ①
    const inputPhoneNumber = '987653498';  ②
    const transformedPhoneNumber = pipe.transform(inputPhoneNumber);
    const expectedResult = '987653498';  ②
    expect(transformedPhoneNumber).toBe(expectedResult);
  })

});
  • ① → déclaration du test dans la sous suite « default behavior ».
  • ② → la chaine 987653498 n’a que 9 caractères, donc moins de 10.
  • ③ → le résultat attendu est donc 987653498

Conclusion

Étant donné que les « pipes » ne prennent qu’une valeur en entrée, transforment cette valeur, puis renvoient le résultat transformé, écrire des tests pour ces classes est très simple. C’est aussi parcequ’elles font partie des « fonctions pures », et n’ont donc pas pas de « sides effects » (ou effets de bord).

Les « sides effects » sont des changements qui se produisent en dehors d’une fonction après l’exécution de la dite fonction. Un « sides effects » très courant est, par exemple, un affiche dans la console d’un message d’erreur.

Lorsque vous testez des « pipes », vous testez principalement la méthode de transformation (transform) incluse dans la classe car c’est elle qui :

  • reçoit les différents paramètres que vous souhaitez manipuler.
  • effectue la manipulation.
  • puis renvoie les valeurs modifiées.