Protractor – A l’assaut des tests E2E avec Angular

Les tests restent un vaste sujet sur lequel chaque projet et chaque développeur devrait réellement se tourner. Il existe évidemment plusieurs types de test. Par exemple, les tests unitaires sont là pour tester et valider un morceau de code bien particulier pour une fonction. Ils sont faciles à écrire, et ils garantissent un comportement bien précis du système à un instant T. Toute refactorisation ou modification du code doit garantir que les anciens tests fonctionnent toujours afin de s’assurer qu’on a rien casser.

Cependant, pouvant nous garantir que tous les cas sont bien testés ? Que se passe-t-il lorsque des cas d’utilisations croisent plusieurs fonctionnalités différentes de l’application ? Quel est le comportement de l’application lorsque les performances sont mauvaises ? Comment tester dans mon cas qu’une modification dans le système a bien été répercuté 3 écrans plus loin ? Pour répondre à ces questions, les tests unitaires ne suffisent plus car leur périmètre est bien trop restreint. C’est là que Protractor vient nous aider avec les tests end-to-end.

Les tests end-to-end (ou E2E) sont particulièrement utiles lorsqu’on veut tester des fonctionnalités très complètes où le développeur a besoin d’une grande partie de l’application. Leur but est notamment de tester certains modules complexes et critiques de l’application (enchaînement de plusieurs pages, interaction avec de multiples composants …) afin de garantir que chaque nouvelle version de l’application conserve le comportement souhaité. En pratique, les tests E2E vont venir simuler des interactions utilisateurs avec un navigateur : clique sur un bouton, écriture dans une zone de saisie, vérification de contenu … Pour cet article, nous allons nous baser sur une nouvelle application Angular généré à partir de Angular CLI.

> ng new TestE2E

Vous avez dit Protractor ?

Protractor est un Framework de test E2E développé pour Angular et AngularJS. C’est un superset de Selenium et se base sur le même driver que ce dernier afin d’envoyer des commandes au navigateur. Le développeur va pouvoir écrire des tests (en Typescript) et des commandes comme si c’était l’utilisateur final qui manipule le navigateur. Il va ainsi pouvoir :

  • Ecrire dans des champs de saisie ;
  • Cliquer sur des boutons ;
  • Rechercher des éléments dans le DOM par leur contenu ;
  • Faire bouger la souris à des endroits bien précis.

Le Framework a été spécialement conçu pour Angular, cela veut dire qu’il intègre des APIs et des mécanismes uniques permettant de tester des éléments spécifiques à Angular sans effort particulier. De plus, nous savons qu’Angular effectue souvent des opérations asynchrones (appel API, détection de changement …) et Protractor est capable d’attendre n’importe quel tâche avant de continuer l’exécution du test.

La configuration de Protractor

Tout d’abord, voyons ce que Angular CLI a généré pour nous au niveau du fichier protractor.conf.js. Vous l’aurez compris, ce fichier permet de configurer Protractor lors de son lancement.

exports.config = {
  allScriptsTimeout: 11000,
  specs: [
    './e2e/**/*.e2e-spec.ts'
  ],
  capabilities: {
    'browserName': 'chrome'
  },
  directConnect: true,
  baseUrl: 'http://localhost:4200/',
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    print: function() {}
  },
  onPrepare() {
    require('ts-node').register({
      project: 'e2e/tsconfig.e2e.json'
    });
    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
  }
};

Les différentes options sont les suivantes :

  • allScriptsTimeout: définis le temps d’attente de Protractor pendant que Angular fait des choses en arrière-plan ;
  • specs: chemin vers les fichiers .spec de test de Protractor. Les chemins peuvent contenir des glob patterns afin de simplifier les écritures ;
  • directConnect : option afin que Protractor communique directement avec le navigateur sans passer par le driver Selenium. Cette fonctionnalité fonctionne avec Chrome ou Firefox, permettant ainsi que les scripts se lancent plus vite au démarrage ;
  • baseUrl : URL sur laquelle Protractor va envoyer les commandes ;
  • framework : framework de test utilisé par Protractor (par défaut Jasmine) ;
  • jasmineNodeOpts : paramétrage du framework de test. Dans l’exemple ci-dessus :
    • Les couleurs seront affichés dans le terminal ;
    • Les tests échoueront après 30s d’inactivité ;
    • L’affichage des résultats a été modifiés.
  • onPrepare: fonction exécuté avant le lancement des tests. Dans l’exemple, on indique quel fichier de configuration le transpilateur TypeScript doit utiliser. Ensuite, on ajoute un reporteur pour l’affichage des résultats à Jasmine.

Lancement des tests

Après avoir analyser notre configuration, pourquoi ne pas lancer nos tests ? Il suffit de lancer la commande suivante :

>  ng e2e

Cette commande d’Angular CLI va lancer Protractor et les tests associés. Dans la console, on voit que l’outil compile le projet, le lance et exécute les tests en retournant le résultat dans le terminal.

Lancement des tests Protractor

Lancement des tests Protractor

Note : on peut noter au passage que Protractor a bien utiliser directement le driver de Chrome, et non le driver de Selenium pour communiquer avec le navigateur.

Regardons maintenant d’un peu plus près le fichier de test app.e2e-spec.ts.

import { AppPage } from './app.po';

describe('test-e2-e App', () => {
  let page: AppPage;

  beforeEach(() => {
    page = new AppPage();
  });

  it('should display welcome message', () => {
    page.navigateTo();
    expect(page.getParagraphText()).toEqual('Welcome to app!');
  });
});

Les mots-clés importants ici sont :

  • describe: définit un jeu de un ou plusieurs tests qui sont regroupés sous un seule libellé ;
  • beforeEach: définit la fonction qui va être appelé avant chaque exécution de test. Il existe également beforeAll, afterEach et afterAll.
  • it : définit un test qui va être exécute par le moteur.

Ceci suffit à Protractor pour détecter les tests à exécuter.

Un pattern est régulièrement utilisé dans les tests E2E : le Page Objects pattern. Ce pattern est très simple : il encapsule les informations de la page qui va être testé dans un objet TypeScript afin de les manipuler plus aisément : recherche d’élément dans la page, saisie de texte dans une zone de saisie, clique sur un bouton … Cette technique a plusieurs avantages :

  • Réutilisation de l’objet dans d’autres au besoin ;
  • Simplification et meilleure lisibilité du test. Le code du test ne fait qu’utiliser des expect;
  • Lors d’un changement dans le test, il suffit de changer qu’à un endroit le code pour répercuter les changements dans tous les tests.

Dans notre exemple, le Page Object utilisé fait 2 choses :

  • navigateTo: navigation vers la page concerné ;
  • getParagraphText: il récupère le contenu de l’élément HTML suivant le sélecteur app-root h1.
export class AppPage {
  navigateTo() {
    return browser.get('/');
  }

  getParagraphText() {
    return element(by.css('app-root h1')).getText();
  }
}

Quelques cas plus complexes

Pour aller plus loin dans nos tests E2E, nous allons tester de la saisie et des cliques sur des boutons afin de valider que notre application fonctionne correctement. Rajouter les morceaux de code suivant.

app.component.html

<div *ngFor="let name of names">
  <label>Nom :</label>
  <input type="text" [(ngModel)]="name.text" required>
</div>

<button (click)="removeName()">Supprimer un nom</button>
<button (click)="addName()">Ajouter un nom</button>

app.component.html

names: { text?: string }[] = [];

public addName(): void {
  this.names.push({});
}

public removeName(): void {
  this.names.pop();
}

Note : n’oubliez pas d’importer FormsModule dans AppModule.

Le code rajouté est très simple et met en œuvre plusieurs fonctionnalités d’Angular comme :

  • clique d’un bouton ;
  • boucle sur tableau ;
  • binding bidirectionnel via ngModel;
  • binding unidirectionnel.

Commençons par étoffer notre Page Object afin de manipuler notre page plus facilement. Nous allons rajouter les méthodes suivantes :

  • addName: ajout d’un nom dans notre collection ;
  • removeName: suppression d’un nom ;
  • sendKeysToInputNameByIndex: écriture dans une zone de saisie. Comme nous avons une boucle, l’index va nous permettre d’identifier précisément dans quelle zone nous voulons écrire ;
  • getTextContenetByIndex : récupère le contenu du texte bindé à la variable text, toujours selon un index précis (car il peut y en avoir plusieurs).
  • getNumberOfInput : récupère le nombre total de zone saisissable.
addName() {
  return element(by.cssContainingText('button', 'Ajouter un nom')).click();
}

removeName() {
  return element(by.cssContainingText('button', 'Supprimer un nom')).click();
}

sendKeysToInputNameByIndex(inputIndex: number, text: string) {
  return $$('input').get(inputIndex).sendKeys(text);
}

getTextContentByIndex(spanIndex: number) {
  return $$('span').get(spanIndex).getText();
}

getNumberofInput() {
  return $$('input').count();
}

Nos cas de tests seront les suivants :

  • cas 1: ajout d’un nom, écriture dans la zone de saisie et vérification que le nom est correct ;
  • cas 2: ajour d’un deuxième avec un nom différent, vérification que le nom est correct et qu’il est différent du premier ;
  • cas 3: suppression d’un nom, vérification qu’il n’en reste plus qu’un ;
// Cas 1
it('should add TEST name', () => {
  page.navigateTo();
  page.addName();

  // Ecriture dans le première zone de saisie
  page.sendKeysToInputNameByIndex(0, 'TEST');

  expect(page.getTextContentByIndex(0)).toEqual('Le nom est TEST');
});

// Cas 2
it('should add TEST2 name', () => {
  page.addName();

  // Ecriture dans le deuxième zone de saisie
  page.sendKeysToInputNameByIndex(1, 'TEST2');

  // Vérification des 2 textes
  expect(page.getTextContentByIndex(1)).toEqual('Le nom est TEST2');
  expect(page.getTextContentByIndex(0)).toEqual('Le nom est TEST');
  expect(page.getNumberOfInput()).toEqual(2);
});

// Cas 3
it('should remove last name', () => {
  page.removeName();

  // Vérification du nombre
  expect(page.getNumberOfInput()).toEqual(1);
});

Dès la première lecture, les tests sont très lisibles et compréhensibles grâce notamment à notre Page Object. Quelques remarques :

  • Les méthodes d’actions de notre Page Object retournent tout le temps les actions effectués par Protractor (getText(), sendKeys() …). Cela est dû au fait que ces méthodes retournent des Promises, et Protractor sait les attendre automatiquement si leur exécution est répercuté à la fonction racine du test (donc le it) ;
  • Les tests sont dépendants les uns des autres. En effet, le premier test rajoute le premier nom et teste que ce dernier est valide. Pour le deuxième test, on ne rejoue pas tout le scénario du premier test car cela serait trop fastidieux. Les scénarios sont donc liés et donc les it sont liés entre eux. Il est donc important de conserver des tests liés dans des describe consistent, et ce sont les describe qui doivent être indépendant les uns des autres.

Après lancement on peut voir le résultat positif de nos tests.

Résultat des tests Protractor

Résultat des tests Protractor

Conclusion

Les tests E2E sont là afin de tester des cas complexes dans l’application : nos cas de tests auraient facilement pu être écrit en tests unitaires. Cependant, ils illustrent assez bien l’utilisation de l’outil sur des cas simple. Cependant, il est clair que l’écriture des E2E n’est qu’une étape avancée dans le testing Angular : il faut d’abord s’assurer d’avoir écrit tous les tests unitaires avant de s’attaquer aux tests E2E. De plus, les tests E2E peuvent être chronophage en temps de maintenabilité car selon les cas ils peuvent vitre devenir instable. Il faut donc prendre soin et y apporter un soin tout particulier si on veut qu’ils restent efficaces.

Faites tourner ! Share on Facebook
Facebook
Tweet about this on Twitter
Twitter
Share on LinkedIn
Linkedin

Laisser un commentaire

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