Capture d'écran d'un IDE présentant un extrait de code utilisant ng-template avec la mise en avant du logo Angular avec une tinte verte représentant les tests unitaires

Angular : Tester ngTemplateOutlet et createEmbeddedView

Dans mon dernier article, j’ai expliqué comment utiliser ngTemplateOutlet et createEmbeddedView pour créer des vues dynamiques avec ng-template dans une application Angular. Bien que rarement utilisées, ces fonctionnalités sont extrêmement utiles pour développer des composants réutilisables et flexibles. Cependant, tester ces vues dynamiques est un réel défi car elles dépendent d’un composant parent. Dans cet article, nous allons explorer comment tester ngTemplateOutlet et createEmbeddedView en utilisant des tests unitaires avec Jasmine et Karma.

Écrire des tests unitaires utiles

Comme mentionné dans mon livre « Apprendre à programmer ses premières applications », les tests unitaires sont essentiels pour garantir la qualité et la fiabilité de votre code. Cependant, au même titre que le reste de votre application, les tests unitaires sont du code source qui doit être maintenu. Il est donc important d’écrire des tests unitaires utiles qui aident à identifier les erreurs et à valider le comportement de vos unités de code (fonctions, méthodes, composants, etc.).

Les composants Angular utilisant la directive ngTemplateOutlet pour afficher des vues dynamiques ne devraient pas tester le contenu de ces vues. Cette directive appartient au framework Angular et est déjà testée par leur équipe de développement. Nous devrions donc nous concentrer sur le comportement métier de notre composant.

Par exemple, dans le projet démo de l’article précédent, la pertinence des tests unitaires pour le composant TableComponent est assez limitée. En revanche, le composant KanbanBoardComponent, qui instancie dynamiquement les cartes de tâches et les déplace entre les colonnes, est un bon candidat pour des tests unitaires. En testant le comportement de ce composant, nous nous assurons qu’après un déplacement, les vues dynamiques restent cohérentes avec l’état interne du composant.

Tester createEmbeddedView avec Jasmine et Karma

Le projet Angular de démonstration a été généré avec Angular CLI qui définit par défaut une configuration de test basée sur Jasmine et Karma. Nous allons donc partir des fichiers *.spec.ts existants pour écrire nos tests unitaires.

Comment fournir un TemplateRef à un composant ?

Cette étape est primordiale pour tester un composant qui utilise la méthode createEmbeddedView pour construire dynamiquement sa vue. Nous devons donc fournir un TemplateRef valide à notre composant pour qu’il puisse instancier la vue correctement.

Pour ce faire, il existe deux approches :

  • Créer un TemplateRef factice dans le test unitaire.
  • Utiliser un ng-template grâce à un composant parent.

La meilleure approche est sans aucun doute la seconde, car le TemplateRef fourni à notre composant se comportera exactement comme dans l’application réelle, sans avoir à redéfinir tout un tas de méthodes. Cependant, cette approche nécessite de créer un composant parent qui contient le ng-template à tester.

Créer un composant parent pour les tests unitaires

L’objectif est de créer un composant parent dans le scope du test unitaire qui contient le ng-template à tester. Ce composant sera similaire à celui du KanbanPageComponent de l’article précédent, mais avec un contenu propice aux tests unitaires. Sa définition devrait ressembler à la suivante :

 // /src/app/components/kanban-board/kanban-board.component.spec.ts
+import { Component, ViewChild } from '@angular/core';
+
 import { ComponentFixture, TestBed } from '@angular/core/testing';

 import { KanbanBoardComponent } from './kanban-board.component';

-describe('KanbanBoardComponent', () => {
+
+@Component({
+  standalone: true,
+  imports: [KanbanBoardComponent],
+  template: `<app-kanban-board>
+    <ng-template #ticket let-ticket>
+      <h1>{{ ticket.title }}</h1>
+    </ng-template>
+  </app-kanban-board>`,
+})
+class ParentTestComponent { <1>
+  @ViewChild(KanbanBoardComponent, { static: true })
+  public board!: KanbanBoardComponent; <3>
+}
+
+
+describe('KanbanBoardComponent', () => {
   let component: KanbanBoardComponent;
-  let fixture: ComponentFixture<KanbanBoardComponent>;
+  let fixture: ComponentFixture<ParentTestComponent>;

   beforeEach(async () => {
 // ...
     .compileComponents();

-    fixture = TestBed.createComponent(KanbanBoardComponent);
-    component = fixture.componentInstance;
+    fixture = TestBed.createComponent(ParentTestComponent); <2>
+    component = fixture.componentInstance.board; <3>
+
+    // Configure default board data
+    component.tickets = {
+      todo: [ { title: 'TODO', description: '' } ],
+      inProgress: [ { title: 'IN_PROGRESS', description: '' } ],
+      done: [ { title: 'DONE', description: '' } ],
+    }
+
     fixture.detectChanges();
   });
 // ...
1

Nous déclarons un composant parent contenant le template qui fournit le ng-template au KanbanBoardComponent.

2

Nous remplaçons la création du composant KanbanBoardComponent par celle du composant parent.

3

Nous récupérons l’instance du composant KanbanBoardComponent à partir du composant parent.

En jouant les tests unitaires, vous verrez que le cas de test KanbanBoardComponent > should create est toujours valide. D’ailleurs, le rapport de couverture indique que la méthode ngAfterContentChecked est correctement appelée, ce qui signifie que notre composant a bien instancié le ng-template fourni par le composant parent.

Tester le déplacement des cartes de tâches

Afin écrire de bons tests unitaires pour le déplacement des cartes de tâches, nous devons nous assurer que le composant KanbanBoardComponent met correctement à jour les colonnes de tâches et ses données internes.

Les tests unitaires suivants nous permettent de valider le comportement des méthodes movePrevious et moveNext du composant KanbanBoardComponent :

 // /src/app/components/kanban-board/kanban-board.component.spec.ts
 // ...
   it('should create', () => {
     expect(component).toBeTruthy();
   });
+
+  it('should move ticket to next column', () => {
+    // Test moving ticket from todo to inProgress
+    component.selectControl!.setValue(component.tickets.todo[0]);
+
+    component.moveNext();
+
+    // Test data after moving ticket
+    expect(component.tickets.todo.length).toBe(0);
+    expect(component.tickets.inProgress.length).toBe(2);
+
+    // Test dynamic template rendering
+    const headers: string[] = [];
+    fixture.nativeElement.querySelectorAll('.board .column:nth-child(2) h1')
+      .forEach((el: any, i: number) => {
+        headers.push(el.innerText);
+      });
+    expect(headers).toEqual(['IN_PROGRESS', 'TODO']);
+    expect(
+      fixture.nativeElement
+        .querySelectorAll('.board .column:nth-child(1) h1')
+        .length
+    ).toBe(0);
+  });
+
+  it('should move ticket to previous column', () => {
+    // Test moving ticket from inProgress to todo
+    component.selectControl!.setValue(component.tickets.done[0]);
+
+    component.movePrevious();
+
+    // Test data after moving ticket
+    expect(component.tickets.inProgress.length).toBe(2);
+    expect(component.tickets.done.length).toBe(0);
+
+    // Test dynamic template rendering
+    const headers: string[] = [];
+    fixture.nativeElement.querySelectorAll('.board .column:nth-child(2) h1')
+      .forEach((el: any, i: number) => {
+        headers.push(el.innerText);
+      });
+    expect(headers).toEqual(['IN_PROGRESS', 'DONE']);
+    expect(
+      fixture.nativeElement
+        .querySelectorAll('.board .column:nth-child(3) h1')
+        .length
+    ).toBe(0);
+  });
 });

Ces deux nouveaux tests unitaires sélectionnent la carte de tâche à déplacer et appellent les méthodes moveNext et movePrevious pour effectuer le déplacement. Dans chaque test, nous vérifions ensuite que les données internes du composant sont correctement mises à jour, tout comme les colonnes de tâches affichées dans la vue dynamique.

Tester exhaustivement les déplacements de cartes

Ces trois premiers tests unitaires permettent de couvrir quasiment 90 % du code du composant KanbanBoardComponent. Cependant, pour tester exhaustivement le composant, il manque les tests de déplacement des cartes de la colonne in progress vers les colonnes todo et done, et surtout les cas où le déplacement est impossible (par exemple, déplacer une carte de la colonne todo vers la gauche). Mais ces tests ne sont pas nécessairement pertinents.

En effet, la méthode privée move est systématiquement appelée lors du déplacement des tickets, les tests relatifs au déplacement depuis la colonne in progress ne serviraient qu’à couvrir les autres branches du code des méthodes moveNext et movePrevious sans apporter de réelle valeur ajoutée à la suite de tests.

En revanche, tester les cas où le déplacement est impossible ne couvrira pas plus de code. Cependant, ces tests sont réellement importants pour valider les scénarios fonctionnels d’utilisation du composant. Par exemple, si un utilisateur veut déplacer une carte de la colonne todo vers la gauche, le composant doit l’en empêcher. Il serait donc judicieux d’ajouter un test unitaire pour vérifier cette fonctionnalité.

Conclusion

En résumé, tester des composants Angular qui utilisent des vues dynamiques avec ngTemplateOutlet et createEmbeddedView peut sembler complexe, mais en utilisant des composants parents pour fournir des TemplateRef, la tâche devient beaucoup plus simple.

Il est important de se rappeler que les tests unitaires doivent être maintenables et pertinents. Ils doivent aider à identifier les erreurs et à valider le comportement du code. En se concentrant sur le comportement métier et en évitant de tester les fonctionnalités déjà couvertes par le framework, nous pouvons écrire des tests unitaires qui apportent une réelle valeur ajoutée à notre projet, même si le même code est couvert par plusieurs tests ou que le taux de couverture n’atteint pas de 100 %.

Si vous souhaitez en savoir plus sur les tests unitaires et améliorer vos compétences en programmation, vous pouvez obtenir mon livre « Apprendre à programmer ses premières applications ». Vous y trouverez des conseils pratiques et des exemples concrets pour écrire des tests unitaires efficaces et maintenir la qualité de votre code.

Laisser un commentaire