Pourquoi Tester Angular avec Jest ?
Tester Angular avec Jest est essentiel pour les développeurs front-end car il permet d'assurer la qualité et la fiabilité de l'application Angular tout au long du développement. Dans un contexte réel, tester régulièrement une application Angular aide à identifier les bugs tôt dans le processus, ce qui peut économiser beaucoup de temps et d'efforts lors des phases ultérieures de développement ou de production.
Un cas d'utilisation concret est la gestion des formulaires. Tester un formulaire Angular avec Jest permet de s'assurer que chaque champ fonctionne correctement, que les validations sont appliquées en temps réel et que le comportement du formulaire sous différents scénarios est conforme aux attentes.
Prerequis
Voici la liste des connaissances nécessaires et des outils à installer :
Connaissances requises :
- Connaissance avancée d'Angular
- Compréhension de Jest, un framework de tests JavaScript
- Familiarité avec TypeScript
Outils à installer :
- Node.js (version recommandée : 14.x ou ultérieure)
- Angular CLI (version recommandée : 12.x ou ultérieure)
Pour installer les outils nécessaires, exécutez les commandes suivantes dans votre terminal :
## Installer Node.js
https://nodejs.org/en/download/
## Installer Angular CLI
npm install -g @angular/cli
Concepts fondamentaux
1. Jest et Angular CLI
Jest est un framework de tests JavaScript populaire qui peut être utilisé avec Angular pour écrire des tests unitaires, d'intégration et de bout à bout.
Angular CLI fournit des outils pour générer les fichiers de test automatiquement. Voici comment créer un nouveau projet Angular :
ng new angular-jest-project
cd angular-jest-project
2. Tests Unitaires avec Jest
Les tests unitaires vérifient le comportement individuel d'un composant, d'un service ou d'une fonction.
Créons un test unitaire pour un simple service :
// src/app/data.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { DataService } from './data.service';
describe('DataService', () => {
let service: DataService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(DataService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should return data', () => {
const expectedData = { message: 'Hello, World!' };
spyOn(service, 'getData').and.returnValue(expectedData);
const result = service.getData();
expect(result).toEqual(expectedData);
});
});
3. Tests d'Intégration avec Jest
Les tests d'intégration vérifient comment les composants et les services s'interconnectent.
Créons un test d'intégration pour un composant qui utilise un service :
// src/app/data.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DataComponent } from './data.component';
import { DataService } from '../data.service';
describe('DataComponent', () => {
let component: DataComponent;
let fixture: ComponentFixture<DataComponent>;
let dataService: DataService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DataComponent ],
providers: [ DataService ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DataComponent);
component = fixture.componentInstance;
dataService = TestBed.inject(DataService);
fixture.detectChanges();
});
it('should display message from service', () => {
const expectedData = { message: 'Hello, World!' };
spyOn(dataService, 'getData').and.returnValue(expectedData);
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.textContent).toContain('Hello, World!');
});
});
4. Tests d'End-to-End avec Jest
Les tests d'end-to-end vérifient le comportement complet de l'application dans un environnement réel.
Créons un test E2E pour une route simple :
// src/app/app.e2e-spec.ts
import { AppPage } from './app.po';
describe('angular-jest-project', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', async () => {
await page.navigateTo();
expect(await page.getTitleText()).toEqual('Welcome to angular-jest-project!');
});
});
Mise en pratique : projet fil rouge
Commençons par créer un gestionnaire de tâches simple avec Angular et Jest.
Étape 1 : Créer le Projet
ng new task-manager
cd task-manager
npm install --save-dev jest @types/jest ts-jest @angular-builders/jest
Étape 2 : Générer les Composants et Services
ng generate component tasks
ng generate service task
Étape 3 : Modifier le Service task.service.ts
// src/app/task.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class TaskService {
private tasks = [
{ id: 1, title: 'Task 1', completed: false },
{ id: 2, title: 'Task 2', completed: true }
];
getTasks() {
return this.tasks;
}
addTask(task) {
this.tasks.push(task);
}
completeTask(id) {
const task = this.tasks.find(t => t.id === id);
if (task) {
task.completed = true;
}
}
}
Étape 4 : Modifier le Composant tasks.component.ts
// src/app/tasks/tasks.component.ts
import { Component, OnInit } from '@angular/core';
import { TaskService } from '../task.service';
@Component({
selector: 'app-tasks',
templateUrl: './tasks.component.html',
styleUrls: ['./tasks.component.css']
})
export class TasksComponent implements OnInit {
tasks = [];
constructor(private taskService: TaskService) {}
ngOnInit(): void {
this.tasks = this.taskService.getTasks();
}
addTask(task) {
this.taskService.addTask(task);
this.tasks = this.taskService.getTasks();
}
completeTask(id) {
this.taskService.completeTask(id);
this.tasks = this.taskService.getTasks();
}
}
Étape 5 : Créer les Tests Unitaires pour le Service
// src/app/task.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { TaskService } from './task.service';
describe('TaskService', () => {
let service: TaskService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(TaskService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should return tasks', () => {
const expectedTasks = [
{ id: 1, title: 'Task 1', completed: false },
{ id: 2, title: 'Task 2', completed: true }
];
expect(service.getTasks()).toEqual(expectedTasks);
});
it('should add a task', () => {
const newTask = { id: 3, title: 'New Task', completed: false };
service.addTask(newTask);
expect(service.getTasks().length).toBe(3);
});
it('should complete a task', () => {
const taskId = 1;
service.completeTask(taskId);
expect(service.getTasks()[0].completed).toBe(true);
});
});
Étape 6 : Créer les Tests d'Intégration pour le Composant
// src/app/tasks/tasks.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TasksComponent } from './tasks.component';
import { TaskService } from '../task.service';
describe('TasksComponent', () => {
let component: TasksComponent;
let fixture: ComponentFixture<TasksComponent>;
let taskService: TaskService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ TasksComponent ],
providers: [ TaskService ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TasksComponent);
component = fixture.componentInstance;
taskService = TestBed.inject(TaskService);
fixture.detectChanges();
});
it('should display tasks', () => {
const expectedTasks = [
{ id: 1, title: 'Task 1', completed: false },
{ id: 2, title: 'Task 2', completed: true }
];
spyOn(taskService, 'getTasks').and.returnValue(expectedTasks);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expectedTasks.forEach(task => {
expect(compiled.querySelector(`[data-task-id="${task.id}"]`).textContent).toContain(task.title);
});
});
it('should add a task', () => {
const newTask = { id: 3, title: 'New Task', completed: false };
const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
inputElement.value = newTask.title;
inputElement.dispatchEvent(new Event('input'));
const addButton = fixture.debugElement.query(By.css('.add-task-button'));
addButton.triggerEventHandler('click', null);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector(`[data-task-id="${newTask.id}"]`).textContent).toContain(newTask.title);
});
it('should complete a task', () => {
const taskId = 1;
const checkboxElement = fixture.debugElement.query(By.css(`input[data-task-id="${taskId}"]`)).nativeElement;
checkboxElement.checked = true;
checkboxElement.dispatchEvent(new Event('change'));
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector(`[data-task-id="${taskId}"]`).textContent).toContain('(completed)');
});
});
Étape 7 : Créer les Tests E2E
// src/app/app.e2e-spec.ts
import { AppPage } from './app.po';
describe('task-manager', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display title', async () => {
await page.navigateTo();
expect(await page.getTitleText()).toEqual('Task Manager');
});
it('should add a task', async () => {
const newTask = 'New Task';
await page.addTask(newTask);
expect(await page.getTaskCount()).toBe(1);
expect(await page.getTaskTitle(0)).toContain(newTask);
});
it('should complete a task', async () => {
await page.completeTask(0);
expect(await page.getTaskStatus(0)).toBe('(completed)');
});
});
Étape 8 : Exécuter les Tests
ng test --watch=false
ng e2e --watch=false
Erreurs fréquentes et debugging
1. TypeError: Cannot read property 'test' of undefined
Cause : Jest n'est pas correctement configuré dans le projet Angular.
Correction :
ng test --watch=false
ng e2e --watch=false
2. TypeError: Cannot read property 'getTasks' of undefined
Cause : Le service n'est pas correctement injecté dans le composant.
Correction :
// src/app/tasks/tasks.component.ts
import { Component, OnInit } from '@angular/core';
import { TaskService } from '../task.service';
@Component({
selector: 'app-tasks',
templateUrl: './tasks.component.html',
styleUrls: ['./tasks.component.css']
})
export class TasksComponent implements OnInit {
tasks = [];
constructor(private taskService: TaskService) {}
ngOnInit(): void {
this.tasks = this.taskService.getTasks();
}
addTask(task) {
this.taskService.addTask(task);
this.tasks = this.taskService.getTasks();
}
completeTask(id) {
this.taskService.completeTask(id);
this.tasks = this.taskService.getTasks();
}
}
3. TypeError: Cannot read property 'click' of null
Cause : L'élément à cliquer n'existe pas dans le DOM.
Correction :
// src/app/tasks/tasks.component.spec.ts
it('should complete a task', () => {
const taskId = 1;
const checkboxElement = fixture.debugElement.query(By.css(`input[data-task-id="${taskId}"]`)).nativeElement;
checkboxElement.checked = true;
checkboxElement.dispatchEvent(new Event('change'));
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector(`[data-task-id="${taskId}"]`).textContent).toContain('(completed)');
});
Pour aller plus loin
Tests Asynchrones : Apprendre à tester des fonctions asynchrones avec Jest.
Code Coverage : Configurer la mesure de couverture de code pour vous assurer que vos tests touchent bien tout le code.
Mocking Services : Apprendre à utiliser des mocks avec Jest pour isoler les composants et services.
Défi pratique
Créez un simple application Angular qui permet d'afficher une liste de livres, d'ajouter de nouveaux livres et de chercher des livres par titre. Assurez-vous de couvrir tous les cas d'utilisation avec des tests unitaires, d'intégration et d'end-to-end.