Tester une app Angular avec Vitest

🏷️ Front-end 📅 10/04/2026 22:00:00 👤 Mezgani Said
Angular Vitest Testing Jasmine Coverage
Tester une app Angular avec Vitest

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.