~6 min de lecture
Vitest config Angular : setup, jsdom et couverture de code
Tu as Vitest qui tourne sur ton projet Angular, mais tu te demandes ce que fait vraiment chaque ligne de
vitest.config.ts ? Ou comment activer une couverture de code propre, avec des seuils qui font échouer la CI quand
on descend trop bas ?
Cet article décortique la configuration Vitest pour Angular, ligne par ligne : le fichier de config, le
test-setup.ts, l'environnement jsdom, et toute la partie couverture (provider, reporters, seuils, exclusions) plus
l'intégration en CI.
Avant de plonger, il faut savoir qu'il existe aujourd'hui deux mondes pour faire tourner Vitest sur Angular :
- Le builder officiel
@angular/build:unit-test(expérimental en Angular 20.2, runner par défaut en 21). La CLI gère la config Vitest et l'initialisation de l'environnement de test pour toi viaangular.json. Tu n'écris quasiment pas devitest.config.tsni detest-setup.ts. - Le plugin AnalogJS (
@analogjs/vite-plugin-angular). Tu écris unvitest.config.tsexplicite plus un fichier de setup. Contrôle total sur chaque option.
La suite de cet article (l'anatomie de vitest.config.ts et le test-setup.ts) détaille la route AnalogJS. La
partie couverture, elle, reste pertinente dans les deux cas : même avec le builder officiel, tu peux personnaliser la
couverture.
Tu pars de zéro et tu viens de Karma ? Lis d'abord Karma to Vitest : migrer ses tests Angular. Pour le détail du mode zoneless, vois le guide Vitest zoneless.
1. Anatomie de vitest.config.ts
Voici une config Angular complète et commentée :
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import angular from '@analogjs/vite-plugin-angular';
export default defineConfig(() => ({
// Le plugin qui sait compiler les composants Angular
plugins: [angular()],
test: {
// Expose describe/it/expect/vi sans import explicite
globals: true,
// Simule un DOM pour tester les composants
environment: 'jsdom',
// Fichier exécuté une fois avant toute la suite
setupFiles: ['src/test-setup.ts'],
// Quels fichiers sont des tests
include: ['src/**/*.spec.ts'],
// Ce qu'on n'exécute pas
exclude: ['node_modules', 'dist'],
},
}));
Détaillons les options qui comptent :
plugins: [angular()]: indispensable. Sans lui, Vitest ne sait pas compiler les templates et décorateurs Angular.globals: true: permet d'écriredescribe(...)sansimport { describe } from 'vitest'. Pratique, mais pense à ajoutervitest/globalsauxtypesde tontsconfig.spec.jsonpour que TypeScript suive.environment: 'jsdom': simule un navigateur léger. Alternative :happy-dom, plus rapide mais parfois moins complet. Pour Angular, jsdom est le choix sûr.setupFiles: pointe vers le fichier qui initialise leTestBed(voir section suivante).include/exclude: restreignent la découverte des tests.
Rappel : si tu es sur le builder officiel
@angular/build:unit-test, cette config est gérée par la CLI viaangular.jsonet tu n'écris pas ce fichier. Le comprendre reste utile pour personnaliser, notamment la couverture (section 3).
2. Le fichier src/test-setup.ts
Ce fichier s'exécute une seule fois avant toute la suite de tests. Son rôle : initialiser l'environnement de test
Angular. Il est propre à la route AnalogJS : avec le builder officiel @angular/build:unit-test, tu n'écris pas ce
fichier, la CLI initialise l'environnement pour toi.
import '@angular/compiler';
import { getTestBed } from '@angular/core/testing';
import {
BrowserTestingModule,
platformBrowserTesting,
} from '@angular/platform-browser/testing';
getTestBed().initTestEnvironment(
BrowserTestingModule,
platformBrowserTesting(),
);
// Mocks globaux jsdom (matchMedia, IntersectionObserver…) : à placer ici, une fois pour toutes.
Ce qu'on y fait :
- On importe
@angular/compilerpour compiler les composants à la volée. - On initialise le
TestBedglobal avecgetTestBed().initTestEnvironment(...). - On garde l'environnement minimal : c'est aussi l'endroit où placer des mocks globaux (par exemple
IntersectionObserveroumatchMedia, absents de jsdom) si plusieurs suites en ont besoin.
Le zoneless va côté configureTestingModule
Angular 21 est zoneless par défaut. Mais ne force pas le zoneless via un @NgModule wrapper dans
initTestEnvironment : la façon idiomatique est de fournir provideZonelessChangeDetection() par test, dans le
TestBed.configureTestingModule({...}) de chaque suite :
import { provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';
TestBed.configureTestingModule({
providers: [provideZonelessChangeDetection()],
});
(C'est bien provideZonelessChangeDetection(), l'API stable, et non l'ancien
provideExperimentalZonelessChangeDetection.)
Aligner le DI des tests sur l'app via appConfig
Plutôt que de reconstruire les providers à la main suite après suite, tu peux partir de la même configuration que
ton app de prod en réutilisant son appConfig :
import { appConfig } from '../app/app.config';
import { TestBed } from '@angular/core/testing';
TestBed.configureTestingModule({
providers: [...appConfig.providers],
});
On ne rajoute pas provideZonelessChangeDetection() ici : si ton app est zoneless, il est déjà dans
appConfig.providers — tu en hérites gratuitement. C'est précisément l'intérêt de partir de la vraie config.
Cette approche colle à un test sociable (style « Detroit » / classiciste) : tu gardes les vrais collaborateurs
in-process (le router et ses vraies routes, les pipes, les services) et tu ne remplaces que ce qui sort du process
ou n'est pas déterministe — typiquement le réseau (provideHttpClient → provideHttpClientTesting()) — en retirant les
providers à effets de bord hors-test (analytics, providers SSR-only qui cassent sous jsdom). Pas de « router de test » :
les vraies routes font partie de la fidélité que tu cherches.
À l'inverse, pour un test unitaire isolé (style « London » / mockiste), ne pars pas de appConfig : construis un
module de test minimal avec uniquement les doublures dont l'unité a besoin.
3. Configurer la couverture de code
La couverture mesure quelle part de ton code est exécutée par les tests. Vitest l'intègre nativement.
Choisir le provider : v8 ou istanbul
Deux options :
v8(recommandé par défaut) : utilise l'instrumentation native du moteur V8. Très rapide, zéro transformation de code supplémentaire.istanbul: plus mature historiquement, parfois plus précis sur le mapping des branches, mais plus lent car il instrumente le code.
Installe le provider correspondant :
# Provider v8 (recommandé)
pnpm add -D @vitest/coverage-v8
# ou provider istanbul
pnpm add -D @vitest/coverage-istanbul
La config de couverture complète
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import angular from '@analogjs/vite-plugin-angular';
export default defineConfig(() => ({
plugins: [angular()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['src/test-setup.ts'],
include: ['src/**/*.spec.ts'],
coverage: {
provider: 'v8',
// Les formats de rapport générés
reporter: ['text', 'html', 'lcov'],
reportsDirectory: './coverage',
// Ne mesure que le vrai code applicatif
include: ['src/**/*.ts'],
exclude: [
'src/**/*.spec.ts',
'src/test-setup.ts',
'src/main.ts',
'src/**/*.config.ts',
'src/**/*.routes.ts',
'**/*.d.ts',
],
// Fait échouer la commande sous ces seuils
thresholds: {
statements: 80,
branches: 75,
functions: 80,
lines: 80,
},
},
},
}));
Les options clés :
reporter:textaffiche un tableau dans le terminal,htmlgénère un rapport navigable dans./coverage, etlcovproduit le format consommé par les outils CI (Codecov, SonarQube, GitLab…).include/exclude: ne mesure que le code qui compte. Exclure les specs, le bootstrap, les fichiers de config et de routes évite de fausser le pourcentage.thresholds: des planchers. Si la couverture passe sous l'un d'eux, la commande de test sort en erreur — parfait pour bloquer une PR qui dégrade la qualité.
Lance la couverture avec :
pnpm vitest run --coverage
N'inclus pas tes fichiers
.spec.tsdans le calcul de couverture, sinon tu mesures la couverture de tes tests. Vérifie aussi quereportsDirectory(par défaut./coverage) est bien dans ton.gitignore.
4. Intégration en CI
Pour la CI, l'objectif est un run non interactif (pas de watch) avec couverture. Ajoute des scripts dans ton
package.json :
{
"scripts": {
"test": "vitest",
"test:ci": "vitest run --coverage"
}
}
vitest sans argument démarre en watch mode (idéal en local), tandis que vitest run exécute une passe unique et
sort — c'est ce qu'il faut en CI.
Exemple d'étape GitHub Actions :
- name: Install
run: pnpm install --frozen-lockfile
- name: Test + coverage
run: pnpm test:ci
Le job échouera automatiquement si un test casse ou si la couverture passe sous tes seuils. Le rapport lcov peut
ensuite être envoyé à ton outil de suivi de couverture.
Sur un monorepo Nx, passe par
nx test(ounx affected -t test) pour ne rejouer que les projets impactés. Nx orchestre Vitest tout en gérant le cache et le graphe de dépendances.
5. Les pièges fréquents de config
- Oublier
vitest/globalsdanstsconfig.spec.json: avecglobals: true, TypeScript râle surdescribe/expecttant que tu n'as pas ajouté"types": ["vitest/globals", "node"]. - jsdom incomplet :
IntersectionObserver,ResizeObserver,matchMedian'existent pas. Mocke-les danstest-setup.tsune fois pour toutes. - Couverture qui inclut tout : sans
include/excludeciblés, tes pourcentages sont faussés par le code de bootstrap et les configs. - Seuils trop ambitieux d'un coup : commence par tes valeurs actuelles, puis remonte les seuils progressivement. Mettre 100 % dès le départ casse la CI et décourage l'équipe.
Conclusion
Une bonne config Vitest pour Angular tient en trois fichiers : un vitest.config.ts clair, un test-setup.ts qui
initialise le TestBed en zoneless, et une section coverage avec des seuils qui protègent ta CI. Une fois ces bases
posées, tu profites d'une boucle de feedback rapide en local et d'un garde-fou solide en intégration continue.
Pour aller plus loin :
- Tu migres depuis Karma ? Karma to Vitest : migrer ses tests Angular
- Le setup détaillé en mode zoneless : Vitest + Angular zoneless
- Monte en compétence sur Angular avec EasyAngularKit