Préface

Un code mal écrit, des fonctionnalités buggées et/ou de mauvaises pratiques de « refactoring » peuvent conduire à des applications peu fiables. Rédiger de bons tests aidera à détecter ces types de problèmes et à les empêcher d’affecter négativement votre application.

Il est essentiel de tester minutieusement votre application si vous souhaitez la faire évoluer durablement.

L’un des principaux objectifs lors de l’écriture de tests est de se prémunir contre les « régressions », c’est à dire des fonctionnalités qui cessent de fonctionner suite à une mise à jour du code, aussi minime soit-elle. Si vous avez déjà développé une application Angular, vous devez savoir que c’est un excellent framework pour créer des applications Web et Web mobiles testables. C’était d’ailleurs l’un des objectifs lors de son élaboration.

Le principal défaut d’Angular est le manque crucial de documentation à son sujet. Même la documentation officielle n’est pas simple à appréhender. Ce manque est encore plus criant lorsqu’on s’intéresse aux tests : un vrai parcours du combattant ! C’est pour cette raison qu’on trouve beaucoup d’applications sans le moindre test ! Les développeurs ont pensé qu’ils perdraient trop de temps à apprendre à tester le framework. Nous verrons combien cette erreur peut être fatale 🤔 !

Un autre défaut d’ Angular qui revient assez souvent : il est complexe à utiliser. Cependant il ne s’agit pas là d’un défaut, mais de son meilleur atout : il est difficile à appréhender car il s’agit d’un des meilleurs frameworks à disposition. Angular est aux framework Js ce que la formule 1 est à l’automobile : la performance avant tout.

Plan global du cours

Tour d’horizon

Je ne vous apprendrai rien, il existe deux types de tests : les tests unitaires et les tests E2E. La majeure partie de mon propos tournera autour de ces deux types de tests.

Les tests unitaires testent des portions de code en « isolation ». J’expliquerai comment écrire des tests unitaires pour les composants, les directives, les « pipes », les services et le routage, à l’aide d’outils ultra connus que sont Karma et Jasmine. Petit rappel succinct de tous ces termes qui devraient vous être familier :

  • composants : morceaux de code utilisés pour « encapsuler » des fonctionnalités qui seront très facilement réutilisable dans toute l’application. Les composants sont un type de directives (voir puce suivante), sauf qu’ils incluent une vue ou un modèle HTML.
  • directives : utilisées pour manipuler des éléments qui existent dans le DOM. Elles peuvent ajouter ou supprimer des éléments du DOM. ngFor, ngIf et ngShow sont des directives fournies par Angular.
  • pipes : utilisé pour transformer les données. Par exemple, pour transformer un entier en devise.
  • services : bien que les services n’existent techniquement pas dans Angular, le concept est toujours important. On utilisera des services pour récupérer des données, puis les injecter dans les composants.
  • routage : permet aux utilisateurs de naviguer d’une vue à l’autre lorsqu’ils effectuent des tâches dans l’application Web.

Introduction aux 2 types de tests

Les tests unitaires et les tests E2E sont les deux types de tests que vous rencontrerez dans ce document. Examinons-les un peu plus en détail 🔎.

Tests unitaires

Les tests unitaires sont utilisés pour tester de portions de code d’une application, généralement des fonctions ou des méthodes. L’objectif de ces tests est de vérifier que chaque « fragment » de code fonctionne correctement (c’est à dire comme attendu)

Ils servent aussi à détecter des erreurs ou des bugs le plus tôt possible dans le processus de développement. Les avantages de l’utilisation massive des tests unitaires sont qu’ils sont rapides, fiables et reproductibles, à condition de les écrire correctement et de les exécuter dans le bon environnement.

Jasmine, qui est un « behavior-driven development framework », est recommandé par Google pour écrire des tests unitaires. Nous l’utiliserons donc tout au long de ce document. Ci-après, le plus basique des tests unitaires :

describe('Exemple de test', () => {
    it('true is true', () => {
        expect(true).toEqual(true); ①
    });
});

Snippet 1.1 – Exemple de test unitaire.

  • ① → Vérification d’intégrité qui vérifie que vrai est égal à vrai.

Le snippet 1.1 fait une seule chose : il vérifie que la valeur booléenne true est égale à la valeur booléenne true. Il s’agit d’un test de contrôle qui ne sert qu’à vérifier que l’environnement de test est fonctionnel.

Ci-après un exemple à peine plus élaboré : on vérifié que les getters/setters de la classe « User » fonctionne comme attendu :

describe('Test User getters and setters', () => {
    it('The user name should be Nicolas', () => {
        const user = new User();
        user.name = 'Nicolas';
        expect(user.name).toEqual('Nicolas'); ①
    });
});

Snippet 1.2 – Un test plus élaboré.

  • ① → Vérifie que le nom de l’utilisateur est le bon.

Bien que les tests unitaires sont plutôt fiables, ils ne reproduisent pas les interactions réelles avec les utilisateurs.

Tests E2E (End to End)

Les tests E2E ont pour but de tester les fonctionnalités d’une application en simulant le comportement de l’utilisateur final. Par exemple, on peut avoir un test E2E qui va vérifier si une fenêtre modale apparaît correctement après la soumission d’un formulaire ou si une page affiche certains éléments lors du chargement de la page, tels que des boutons ou du texte.

Les tests E2E sont vraiment performants du point de vue de l’utilisateur final. Mais ils s’exécutent lentement, et cette lenteur peut être la source de faux positifs qui échouent à cause des comportements asynchrones. Pour cette raison, il est toujours préférable d’écrire des tests unitaires au lieu de tests E2E, et ce dans la mesure du possible. Enfin, notons que l’équipe d’Angular recommande l’utilisation de Protractor E2E.

Etant donné la tâche à accomplir, un test E2E est plus consistant que son homologue unitaire. Le snippet 1.3 propose un test E2E tout simple.

import {browser} from 'protractor';

describe('My App test', () => {    ①
    it('should be the correct title', () => {    ②
        const appUrl = 'http://my-app/';
        const expectedTitle = 'My first E2E test';
        browser.get(appUrl);
        browser.getTitle().then((actualTitle) => {
            expect(actualTitle).toEqual(expectedTitle);    ③
        });
    });
});

Snippet 1.3 – Un test E2E simple

  • ① → Le bloc « describe » résume en une phrase la suite de tests à exécuter.
  • ② → Le bloc « it » indique le commencement d’un test. Le premier paramètre contient un bref descriptif de ce qui est attendu, et le deuxième paramètre est une fonction callback qui va décrire le test.
  • ③ → Le test lui même : le titre doit être équivalent à ce à quoi on s’attend.

Tests unitaires vs tests E2E

Les tests unitaires et les tests E2E présentent des avantages différents. Nous avons déjà un peu discuté de ces avantages, le tableau 1.1, condense tout ce qui a été dit auparavent.

Caractéristique Test unitaire Test E2E
Rapidité Rapide Lent
Fiabilité Très bonne Moyenne (code asynchrone)
Renforcement de la qualité du code Permet d’identifier un code inutilement complexe N’aide pas à écrire un code meilleur car l’application est testée « depuis l’extérieur ».
Coût Faible car rapide à écrire Plus élevé que les tests unitaires car leur écriture prend plus de temps, l’exécution est lente et les tests peuvent être irréguliers, et la fiabilité douteuse
Reproduction des interactions utilisateur Possible mais les interactions complexes ne sont pas testables Le point fort des tests E2E. Par design ils sont fait pour tester les interactions
Tableau 1.1 : comparaison tests unitaires et E2E.

Examinons chacune de ces fonctionnalités une par une :

  • vitesse : on l’a vu, les tests unitaires fonctionnent sur de petits morceaux de code, ils peuvent donc s’exécuter rapidement. Les tests E2E reposent sur des tests via un navigateur, ils ont donc tendance à être plus lents, parfois même très lent.
  • fiabilité : étant donné que les tests E2E ont tendance à impliquer davantage de dépendances et d’interactions complexes, ils peuvent être instables, ce qui peut entraîner des faux positifs. L’exécution de tests unitaires donne rarement des faux positifs. Si un test unitaire bien écrit échoue, vous pouvez être sûr qu’il y a un problème avec le code.
  • renforcement de la qualité du code : l’un des principaux avantages de l’écriture de tests est qu’elle contribue à renforcer la qualité du code. L’écriture de tests unitaires peut aider à identifier un code inutilement complexe qui peut être difficile à tester. En règle générale, si vous avez du mal à écrire des tests unitaires, votre code peut être trop complexe et nécessiter une refactorisation.

Je l’ai déjà dit, mais insistons bien sur ce point : écrire des tests E2E n’aidera jamais à écrire un code de meilleure qualité. Car les tests E2E testent d’un point de vue du navigateur, ils ne testent donc pas directement le code.

  • rentabilité : parce que les tests E2E prennent plus de temps à s’exécuter et peuvent échouer à des moments aléatoires, un coût doit être associé à ce temps. On ne peut pas dire que ce temps soit perdu, mais il a un certain coût. L’écriture de tels tests peut également prendre plus de temps, car ils s’appuyent sur des interactions complexes qui peuvent échouer, si bien que les coûts de développement augmentés en conséquence.
  • reproduction des interactions des utilisateurs : leur capacité à imiter les interactions des utilisateurs en font des tests très puissants, au plus près de la réalité. À l’aide de Protractor, il est possible d’écrire et exécuter des tests comme si un véritable utilisateur interagissait avec l’interface utilisateur.

En résumé, les deux types de tests sont importants pour tester minutieusement une application Angular. Les tests unitaires couvrent un large spectre de possibilités, mais on ne saura jamais de se passer des tests E2E qui prendront en charge les fonctionnaires clés de l’application.

En règle générale, il y aura toujours plus de tests unitaires que de tests E2E, comme le montre la désormais célèbre pyramide de Mike Corn :

Figure 1.1 : répartition des types de tests.

Vous avez peut-être remarqué que la pyramide comprend des tests d’intégration. Les tests d’intégration et les tests E2E sont similaires en ce qu’ils vérifient que les différentes parties d’un système fonctionnent bien ensemble, mais ils ont des objectifs et des scopes différents. Pour nos besoins, nous allons déployer les tests d’intégration dans les tests E2E par soucis de simplicité.

Conclusion

  • Angular fournit un excellent cadre (c’est un framework 😎) pour créer des applications Web et mobiles facilement testables. Il s’appuie fortement sur le concept de composants pour développer des applications.
  • il existe 2 types de tests lors du développement d’applications : les tests unitaires et les tests E2E. Nous utiliserons des tests unitaires pour tester le code brut, tandis les tests E2E nous permettrons de simuler les interactions de l’utilisateur.
  • Protractor, Jasmine et Karma sont les principaux outils recommandés pour tester des applications Angular. Ces outils sont eux-même écrits en JavaScript et s’exécutent automatiquement pendant le développement.