Angular
Vitest
Testing
Jasmine
Coverage
Testing Angular modernes avec Vitest : setup, tester composants/services, async/HTTP, couverture et bonnes pratiques. Plus rapide que Karma.
Vitest vs Jasmine/Karma
Historiquement, Angular utilise Jasmine + Karma pour les tests. Mais Vitest est plus moderne, plus rapide et plus simple à configurer.
| Critère | Jasmine + Karma | Vitest |
|---|---|---|
| Vitesse | Lent (full Webpack build) | ⚡ Très rapide (Vite) |
| Configuration | Complexe (karma.conf.js) | Simple (vitest.config.ts) |
| API | Propriétaire à Jasmine | Compatible Jest/Vitest |
| TypeScript | Support basique | ✅ Support natif |
| Watch mode | Lent à réagir | ⚡ Instantané |
| Couverture | Istanbul (lent) | V8 (rapide) |
À retenir : Vitest est l'avenir du testing Angular. Jasmine reste compatible mais Vitest gagne du terrain.
Setup Vitest dans Angular
1. Installation
npm install -D vitest @vitest/ui happy-dom @angular/platform-browser-dynamic
2. Configuration (vitest.config.ts)
import { defineConfig } from 'vitest/config';
import angular from '@vitejs/plugin-angular';
export default defineConfig({
plugins: [angular()],
test: {
globals: true, // Pas besoin d'importer describe, it, expect
environment: 'happy-dom', // Environnement de test léger
setupFiles: [],
include: ['src/**/*.spec.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/test.ts',
]
}
}
});
3. Configurer Angular pour Vitest (angular.json)
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js"
}
}
4. Package.json scripts
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
}
Lancer les tests :
npm test # Watch mode
npm run test:ui # Interface visuelle
npm run test:coverage # Rapport de couverture
Tester des composants Angular
Composant à tester :
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div>
<button (click)="decrement()">-</button>
<span>{{ count }}</span>
<button (click)="increment()">+</button>
</div>
`
})
export class CounterComponent {
@Input() initialValue = 0;
@Output() countChanged = new EventEmitter<number>();
count = this.initialValue;
increment() {
this.count++;
this.countChanged.emit(this.count);
}
decrement() {
this.count--;
this.countChanged.emit(this.count);
}
}
Tests (counter.component.spec.ts) :
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';
import { describe, it, expect, beforeEach } from 'vitest';
describe('CounterComponent', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CounterComponent]
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should increment count', () => {
component.increment();
expect(component.count).toBe(1);
});
it('should decrement count', () => {
component.count = 5;
component.decrement();
expect(component.count).toBe(4);
});
it('should emit countChanged event', () => {
let emittedValue = 0;
component.countChanged.subscribe((value) => {
emittedValue = value;
});
component.increment();
expect(emittedValue).toBe(1);
});
it('should render count in template', () => {
component.count = 42;
fixture.detectChanges();
const span = fixture.nativeElement.querySelector('span');
expect(span.textContent).toBe('42');
});
it('should initialize with input value', () => {
component.initialValue = 10;
component.count = component.initialValue;
expect(component.count).toBe(10);
});
});
Tester des services et HTTP
Service HTTP :
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) {}
getUser(id: number): Observable<{ id: number; name: string }> {
return this.http.get<{ id: number; name: string }>(`/api/users/${id}`);
}
createUser(data: any): Observable<any> {
return this.http.post('/api/users', data);
}
}
Tests du service :
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
// Vérifier qu'il n'y a pas de requêtes HTTP en attente
httpMock.verify();
});
it('should fetch a user', () => {
const mockUser = { id: 1, name: 'John' };
service.getUser(1).subscribe((user) => {
expect(user.id).toBe(1);
expect(user.name).toBe('John');
});
const req = httpMock.expectOne('/api/users/1');
expect(req.request.method).toBe('GET');
req.flush(mockUser);
});
it('should create a user', () => {
const newUser = { name: 'Jane' };
const response = { id: 2, ...newUser };
service.createUser(newUser).subscribe((user) => {
expect(user.id).toBe(2);
expect(user.name).toBe('Jane');
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual(newUser);
req.flush(response);
});
});
Tester du code asynchrone
Avec observables :
it('should handle async operations with fakeAsync', fakeAsync(() => {
let result: number | undefined;
timer(1000).subscribe((value) => {
result = value;
});
expect(result).toBeUndefined();
tick(1000); // Avancer le temps de 1000ms
expect(result).toBe(0);
}));
// Ou avec waitForAsync
it('should handle async with waitForAsync', waitForAsync(() => {
let result = 0;
timer(100).subscribe(() => {
result = 42;
});
setTimeout(() => {
expect(result).toBe(42);
}, 150);
}));
Avec promises :
it('should handle promises', async () => {
const promise = Promise.resolve('success');
const result = await promise;
expect(result).toBe('success');
});
it('should test rejected promise', async () => {
const promise = Promise.reject(new Error('Failed'));
try {
await promise;
expect.fail('Should have thrown');
} catch (error: any) {
expect(error.message).toBe('Failed');
}
});
Couverture et bonnes pratiques
Générer un rapport de couverture :
npm run test:coverage
# Résultat
# ============ Coverage summary ============
# Statements : 85.5% ( 45/52 )
# Branches : 92.3% ( 12/13 )
# Functions : 88.9% ( 8/9 )
# Lines : 84.6% ( 44/52 )
# ========================================
Bonnes pratiques :
- ✅ Viser 80%+ de couverture de code (pas 100%)
- ✅ Tester les happy paths ET les erreurs
- ✅ Utiliser des mocks pour les dépendances externes
- ✅ Tester les comportements, pas l'implémentation
- ✅ Garder les tests rapides (< 100ms idealement)
- ✅ Nommer les tests clairement : "should [comportement] when [condition]"
- ❌ Ne pas tester le framework lui-même
- ❌ Éviter les faux positifs (tests qui passent pour de mauvaises raisons)
Ignorer certains fichiers de la couverture :
// vitest.config.ts
coverage: {
exclude: [
'src/**/*.module.ts', // Modules Angular
'src/main.ts', // Fichier bootstrap
'src/**/*.interface.ts', // Interfaces (no logic)
'coverage/' // Dossier coverage lui-même
]
}
Résumé : Vitest est l'outil moderne pour tester Angular. Plus rapide, plus simple, meilleur feedback. Adoptez-le dès aujourd'hui.