Tester son application
Introduction
1. Qu’est-ce qu’un test ?
Les tests sont une composante essentielle du développement logiciel moderne. Ils garantissent non seulement la qualité et la fiabilité du code, mais permettent aussi de détecter les régressions, c’est-à-dire de s’assurer que chaque modification n’altère pas le comportement attendu de l’application, facilitant ainsi la maintenance à long terme.
Angular, grâce à son écosystème riche, fournit un ensemble d’outils et de bonnes pratiques pour implémenter plusieurs types de tests et il est parfois difficile de savoir quand, quoi et comment tester. Ce chapitre propose une approche pratique des tests dans Angular, en explorant à la fois les concepts fondamentaux et les outils à disposition.
Nous allons voir deux principaux types de tests : les tests unitaires (unit tests, ou UT) et les tests de bout en bout (end-to-end, ou e2e), dont voici les définitions :
-
Tests unitaires : ils se concentrent sur les plus petites unités de code, comme une fonction ou un service. Ces tests doivent être rapides et indépendants du reste de l’application. Par exemple, nous avons une fonction qui doit filtrer des éléments selon une règle métier bien précise, et pour assurer son fonctionnement, nous allons vérifier si elle retourne...
Tests basiques
1. Structure d’un test
Dans Angular, chaque fichier de test unitaire porte généralement l’extension ".spec.ts". Par convention, chaque composant, service ou autre entité Angular a son propre fichier de test associé ; d’ailleurs, par défaut, en utilisant la ligne de commande d’Angular pour générer un élément du framework, un fichier de test sera automatiquement créé (tant que vous ne spécifiez pas explicitement l’argument --skip-tests).
Voici un exemple de définition de test avec Jasmine :
describe('Filters', () => {
it('should return even numbers', () => {
const result = isEven(2);
expect(result).toBeTruthy()
});
});
Nous avons donc une suite de tests définie par la fonction describe(), ainsi qu’un test unique via it(). Ces deux fonctions prennent une chaîne de caractères, représentant l’identifiant, puis un callback qui sert d’exécution. Enfin, chaque test a pour but de créer une assertion via l’instruction expect(), basée sur une valeur précise, suivie par un comparateur comme toBe(), toBeTruthy(),toEqual(), etc., et c’est ce qui déterminera si le test passe ou non.
Globalement, chaque niveau a son importance : nous voulons une suite de tests relatifs à une fonctionnalité spécifique. Dans cette suite : un ou plusieurs tests pour assurer les différents cas d’exécution et aspects de la fonctionnalité, puis finalement, l’intérieur d’un test peut comporter autant d’assertions que nécessaire pour une bonne couverture du besoin.
À côté des tests, nous pouvons...
Substituer des dépendances dans les tests unitaires
1. Vocabulaire
Dans le cadre des tests unitaires, plusieurs termes sont utilisés pour désigner les différents éléments qui nous aident à tester les fonctionnalités. De manière générale, comme nous souhaitons tester le plus unitairement possible, nous cherchons à nous isoler de toute logique externe aux éléments que nous testons. Cela nous conduit à créer des substituts pour remplacer le code externe, afin d’assurer des tests précis et indépendants. Ces substituts portent différents noms selon leur objectif et la façon dont ils sont construits : doubles, stubs, mocks, spies, fakes, etc. Voici quelques définitions clés :
-
stub : ou "bouche-trou", est un élément codé en dur pour fournir une valeur spécifique pendant l’exécution du test. Si le code à tester appelle une fonction externe isAdmin(), nous pouvons créer une version de cette fonction qui retourne directement true afin de contrôler l’environnement du test.
-
spy : un espion est un objet fourni par le framework de test qui peut être attaché à une propriété ou une fonction. Il permet d’obtenir des informations sur le comportement du code testé. Exemple : avec un spy, nous pouvons vérifier si notre code a appelé la fonction isAdmin() ou non pendant son exécution.
-
mock : ou substitut, est parfois utilisé comme terme générique pour désigner une donnée ou un objet simulé. Un mock correspond plus précisément à une dépendance modifiée pour les besoins du test. Contrairement au simple stub, un mock peut inclure un spy pour suivre les interactions. Exemple : un mock d’un service d’authentification pourrait contenir une méthode isAdmin() simulée, avec un spy pour vérifier le nombre d’appels.
De notre côté, nous allons généraliser cela et utiliser les termes mock et spy.
2. Créer des substituts
a. Substitutions en général
Les principales méthodes pour...
Tester les éléments fondamentaux d’Angular
Dans cette section, nous allons tester les éléments fondamentaux d’Angular : les composants, pipes, directives, services, éléments de routage et observables. Ces différents éléments constituent les blocs de base d’une application Angular et jouent un rôle essentiel dans la logique, l’interface et les interactions utilisateurs.
Ces exemples ne prétendent pas être exhaustifs, mais ils offrent une méthodologie de base pour tester les comportements principaux et garantir la fiabilité des fonctionnalités Angular.
1. Tester un composant
Tester ces composants permet de garantir leur bon fonctionnement en s’assurant que la logique, mais aussi le rendu et les interactions entre l’utilisateur et le DOM sont conformes à nos attentes.
a. Les fixtures
Avant de commencer, voyons ce que sont les objet appelés "fixtures", qui permettent de manipuler les composants. Une fixture dans les tests unitaires Angular est une enveloppe qui représente une instance d’un composant dans un environnement de test. Créée à l’aide de la méthode TestBed.createComponent(), elle joue un rôle essentiel en fournissant un moyen d’interagir avec le composant et son DOM simulé. La fixture permet non seulement d’accéder à l’instance du composant via fixture.componentInstance, afin d’accéder au DOM et aux propriétés et méthodes d’un composant, mais aussi à sa référence globale via fixture.componentRef, afin de modifier ses inputs par exemple.
Grâce à la fixture, il est également possible de déclencher des cycles de détection des changements avec la méthode fixture.detectChanges(). Cette méthode force Angular à appliquer les modifications liées au databinding et à mettre à jour le DOM, crucial pour tester les interactions utilisateurs ou les effets d’un changement de données sur le rendu du composant.
À noter que l’on peut configurer une fixture pour détecter les changements automatiquement grâce à fixture.autoDetectChanges(true);.
Récupérer des éléments du DOM
Il y a deux moyens de représenter...
Tests end-to-end
Nous l’avons dit, les tests e2e, contrairement aux tests unitaires, se concentrent sur le comportement global de l’application, depuis l’interface utilisateur jusqu’aux interactions avec le back-end. L’objectif est de simuler des scénarios réels en faisant tourner l’application dans un navigateur.
À travers les sections suivantes, nous allons voir comment utiliser Cypress ainsi que Playwright, deux outils distincts, pour vous aider à vous faire un avis et choisir celui qui vous correspond le mieux.
Vous pouvez initialiser un outil directement depuis la commande :
ng e2e

1. Introduction à Cypress
Une fois Cypress installé avec ses dépendances, vous pouvez relancer la commande pour démarrer l’outil. Lors de son exécution, une première application de gestion s’ouvre et vous demande si vous souhaitez utiliser un navigateur web ou Electron pour faire tourner votre application.

Nous choisirons le navigateur, ce qui ouvrira une nouvelle instance isolée et dédiée de celui-ci, avec une interface de gestion des tests.
Cette interface liste tous les tests disponibles dans votre projet, organisés par fichier. Vous devriez y voir un premier fichier de test par défaut.

En cliquant sur un fichier, vous déclencherez l’exécution du test. En temps réel, Cypress vous permet de l’observer appliquer les actions définies dans le test comme les clics, navigations et remplissages de champs de saisie, par exemple.

a. Configuration
Après l’installation, un dossier cypress est ajouté à la racine du projet Angular. Ce dossier contiendra tout le nécessaire aux tests e2e, avec :
-
fixtures/ : ce dossier est là pour contenir tous vos fichiers JSON ou autres, utilisés pour stocker des données de tests statiques. Ces données peuvent être utilisées pour simuler une API, par exemple.
// fixtures/users.json
[{
"name": "Kevin", "id": 2
}]
// test.cy.ts
cy.fixture('users').then(data=> {...})
À noter que l’on peut aussi charger les données JSON via un import "ECMAScript". Cependant, la donnée n’est chargée qu’une fois ; si elle...
Projet fil rouge
1. Tests unitaires
Il est préférable d’écrire les tests au fur et à mesure de la création du projet, voire d’adopter une approche de développement par le test (Test-Driven Development ou TDD). Cependant, dans notre cas, puisque le projet est déjà développé, nous allons créer l’ensemble des tests d’un coup.
Pour organiser les tests, il est recommandé de les placer à proximité des éléments qu’ils valident. Une autre approche consiste à séparer le code des tests dans des dossiers distincts. Nous opterons pour cette seconde approche en créant un dossier src/tests/. À l’intérieur, les tests seront répartis en sous-dossiers en fonction des types d’éléments Angular : composants, services, pipes, etc.
Contrairement aux tests E2E ou au développement, où l’organisation suit généralement une logique métier, les tests unitaires sont conçus pour isoler et valider des éléments spécifiques. Il n’est donc pas nécessaire d’adopter une structure basée sur les domaines métier.
Nous allons survoler plusieurs types d’éléments de notre projet à tester. Bien que nous ne détaillerons pas chaque cas de test un par un, vous trouverez une grande série des tests disponibles dans le code source en ligne sur GitHub.
a. Substituer les stores
Lorsque l’élément à tester dépend d’un SignalStore, plusieurs solutions s’offrent à nous. La première consiste à utiliser le store tel quel, mais cela n’est pas recommandé si ce dernier comporte de nombreuses dépendances ou si sa logique est trop complexe, car cela risque de complexifier le test et d’introduire des effets de bord. Une approche plus adaptée est de créer un objet représentant le store et de le fournir via le TestBed, en utilisant useValue. Cette méthode permet de simplifier les tests en ne se concentrant que sur les comportements nécessaires. Dans notre cas, nous allons créer un objet mock enrichi d’espions, ce qui nous permettra de contrôler les interactions avec le store et de visualiser les données...
Conclusion
Extensions d’éditeur de code
Avant de clore, n’hésitez pas à regarder si votre éditeur prend en charge des extensions pour reconnaître et intégrer les tests. Par exemple, Visual Studio Code possède un panneau "Testing" qui permet de rassembler les tests. Il faudra ajouter une extension suivant les frameworks de tests que vous utilisez, comme en installant l’extension officielle "Playwright Test for VSCode". On pourra ainsi visualiser, configurer, enregistrer des séquences de tests et lancer les tests directement depuis l’éditeur.

Synthèse du chapitre
Les tests sont un outil puissant et souvent sous-estimé dans le développement d’applications. Dans ce chapitre, nous avons découvert les différents types de tests, des tests unitaires aux tests end-to-end (e2e), en passant par les outils et bonnes pratiques pour leur mise en œuvre. Nous avons exploré comment tester les éléments fondamentaux d’Angular, comme les composants, les pipes, les services et les routes, tout en abordant des techniques avancées comme le Marble Testing pour tester les flux RxJS. De plus, nous avons introduit les tests e2e avec Cypress et Playwright, des outils robustes pour valider le comportement global d’une application.
Les tests ne sont pas une finalité, mais un investissement qui apporte...