~6 min de lecture
Karma to Vitest : migrer ses tests Angular sans douleur
Si tu maintiens un projet Angular un peu ancien, tes tests tournent probablement encore sous Karma + Jasmine. Le problème : Karma est officiellement déprécié depuis Angular 16 (annonce de l'équipe Angular en 2023), et Vitest est devenu le runner par défaut des nouveaux projets en Angular 21. Le moment est donc idéal pour migrer.
Dans cet article, on voit pourquoi migrer, comment mettre Vitest en place sur Angular, et surtout la table d'équivalences Jasmine → Vitest pour traduire tes tests existants sans tout réécrire de zéro.
Si tu cherches le détail du setup zoneless avec Vitest, on a un guide dédié : Vitest + Angular zoneless. Ici, on se concentre sur la migration depuis Karma.
1. Pourquoi quitter Karma ?
Plusieurs raisons, et elles s'accumulent :
- Karma est déprécié. L'équipe Angular l'a annoncé en 2023, avec Angular 16. Plus de nouvelles fonctionnalités, maintenance minimale.
- La vitesse. Karma lance un vrai navigateur et recharge tout à chaque run. Vitest tourne sur Vite + esbuild, avec un watch mode quasi instantané et de la parallélisation.
- L'ESM. Vite est nativement ESM ; fini les contorsions de bundling. Vitest profite directement de l'écosystème Vite.
- L'expérience moderne. API proche de Jest, snapshots,
expectriche, mocking intégré (vi), couverture out-of-the-box. - C'est la direction officielle. Depuis Angular 21,
ng newgénère un projet avec Vitest par défaut. Karma appartient au passé.
2. Prérequis avant de migrer
Avant de te lancer, vérifie quelques points :
- Une version d'Angular récente (20+ idéalement). Plus ta version est haute, plus le support Vitest est intégré.
- Un projet standalone (pas de
NgModulelegacy). Ce n'est pas bloquant, mais ça simplifie le setup. - Une CI verte avant de commencer, pour comparer avant/après.
- Du temps pour la table d'équivalences : la migration de l'outillage est rapide, c'est la traduction des spies/mocks qui prend le plus de temps sur un gros projet.
3. Deux chemins pour installer Vitest
Il existe aujourd'hui deux approches. Choisis selon ta version d'Angular.
Option A — Le builder officiel (Angular 20.2+)
Depuis Angular 20.2, un builder expérimental est livré avec la CLI : @angular/build:unit-test. Il s'appuie sur Vitest
sous le capot, sans dépendance tierce.
Active-le dans ton angular.json :
{
"test": {
"builder": "@angular/build:unit-test",
"options": {
"tsConfig": "tsconfig.spec.json",
"runner": "vitest",
"buildTarget": "::development"
}
}
}
Puis installe simplement le runner et l'environnement DOM :
pnpm add -D vitest jsdom
C'est l'option à privilégier sur les projets récents : moins de dépendances, alignée sur la roadmap Angular.
Option B — Le plugin AnalogJS (toutes versions 20+)
Si tu es sur une version plus ancienne ou que tu veux un contrôle total sur vitest.config.ts, passe par le plugin
maintenu par l'équipe AnalogJS, qui sait compiler les composants Angular pour Vitest :
pnpm add -D vitest jsdom @analogjs/vitest-angular @analogjs/vite-plugin-angular
Les deux chemins mènent au même résultat. La suite de l'article reste valable quel que soit ton choix.
4. Désinstaller l'ancien monde
Une fois Vitest en place, on dit adieu à Karma et Jasmine :
pnpm remove \
@types/jasmine jasmine-core karma \
karma-chrome-launcher karma-coverage \
karma-jasmine karma-jasmine-html-reporter
Supprime aussi le fichier karma.conf.js s'il existe encore à la racine.
5. La config Vitest minimale
Avec l'option AnalogJS, crée un vitest.config.ts :
/// <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'],
},
}));
Et le fichier de setup src/test-setup.ts, qui initialise l'environnement de test une seule fois. Ce fichier 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…) : ici, une fois pour toutes.
Pour le mode zoneless (la norme depuis Angular 21), ne passe pas par un @NgModule wrapper dans
initTestEnvironment. La façon idiomatique est de fournir provideZonelessChangeDetection() par test, dans le
TestBed.configureTestingModule({...}) :
import { provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';
TestBed.configureTestingModule({
providers: [provideZonelessChangeDetection()],
});
Si tes tests détectent encore le changement via Zone.js, ils risquent de ne pas voir les mises à jour en zoneless. On détaille ce point juste après.
6. Le cœur de la migration : Jasmine vers Vitest
Bonne nouvelle : l'API est très proche. describe, it, expect, beforeEach existent à l'identique. L'essentiel
du travail concerne les spies et les mocks.
| Jasmine / Karma | Vitest | Note |
|---|---|---|
describe / it / expect |
describe / it / expect |
Identique |
beforeEach / afterEach |
beforeEach / afterEach |
Identique |
jasmine.createSpy() |
vi.fn() |
Crée une fonction espionne |
spyOn(obj, 'method') |
vi.spyOn(obj, 'method') |
Espionne une méthode existante |
.and.returnValue(x) |
.mockReturnValue(x) |
Valeur de retour fixe |
.and.callFake(fn) |
.mockImplementation(fn) |
Implémentation custom |
.and.callThrough() |
vi.spyOn(...) (comportement par défaut) |
Vitest passe par défaut |
jasmine.createSpyObj('s', ['a']) |
{ a: vi.fn() } |
Objet de mocks à la main |
expect(spy).toHaveBeenCalled() |
expect(spy).toHaveBeenCalled() |
Identique |
expect(x).toEqual(y) |
expect(x).toEqual(y) |
Identique |
expect(x).toBe(y) |
expect(x).toBe(y) |
Identique |
jasmine.clock() |
vi.useFakeTimers() |
Faux timers |
jasmine.any(Type) |
expect.any(Type) |
Matcher de type |
| (reset auto) | vi.clearAllMocks() |
À appeler dans beforeEach si besoin |
Avant (Jasmine)
describe('UserService', () => {
it('charge un utilisateur', () => {
const http = jasmine.createSpyObj('HttpClient', ['get']);
http.get.and.returnValue(of({ id: 1, name: 'Ada' }));
const service = new UserService(http);
service.load(1);
expect(http.get).toHaveBeenCalledWith('/api/users/1');
});
});
Après (Vitest)
import { describe, it, expect, vi } from 'vitest';
import { of } from 'rxjs';
describe('UserService', () => {
it('charge un utilisateur', () => {
const http = { get: vi.fn().mockReturnValue(of({ id: 1, name: 'Ada' })) };
const service = new UserService(http as any);
service.load(1);
expect(http.get).toHaveBeenCalledWith('/api/users/1');
});
});
Avec globals: true dans la config, tu peux même omettre l'import de describe/it/expect/vi. À toi de voir : les
imports explicites restent plus clairs pour l'IDE.
7. Les pièges à connaître
jsdom n'est pas un vrai navigateur
Vitest tourne sous jsdom, pas Chrome. La plupart des tests passent sans souci, mais certaines API navigateur
(layout, IntersectionObserver, matchMedia) doivent être mockées. Pour des tests qui exigent un vrai moteur de
rendu, regarde le mode navigateur de Vitest (Playwright), mais pour 95 % des tests unitaires, jsdom suffit.
Le zoneless change la détection de changement
En zoneless, plus de Zone.js pour déclencher automatiquement la détection de changement. Dans tes tests de composants,
pense à appeler fixture.detectChanges() explicitement, et privilégie l'API moderne basée sur les signals. C'est
exactement le sujet de notre guide Vitest zoneless.
Le TestBed reste le même
Bonne nouvelle : TestBed.configureTestingModule({...}), TestBed.inject(), TestBed.createComponent() fonctionnent à
l'identique. Tu ne touches pas à la logique de tes tests de composants, juste aux spies. Pour aligner le DI de tes tests
sur l'app réelle, tu peux réutiliser ton appConfig.providers comme point de départ iso, à curer ensuite : on détaille
ce pattern dans le guide config Vitest.
done callback → async/await
Jasmine permettait un callback done. Avec Vitest, préfère les fonctions async et await, plus lisibles et sans
risque de timeout silencieux.
8. Karma vs Vitest en un coup d'œil
| Critère | Karma + Jasmine | Vitest |
|---|---|---|
| Statut | Déprécié | Runner par défaut Angular 21+ |
| Moteur | Vrai navigateur | jsdom (ou navigateur via Playwright) |
| Vitesse | Lente, recharge complète | Rapide, esbuild + parallélisation |
| Watch mode | Lourd | Quasi instantané (HMR) |
| Modules | CommonJS / SystemJS | ESM natif |
| Mocking | jasmine.createSpy |
vi.fn / vi.mock |
| Couverture | karma-coverage |
v8 / istanbul intégrés |
| Config | karma.conf.js |
vitest.config.ts ou builder CLI |
Conclusion
Migrer de Karma à Vitest se résume à trois mouvements : installer Vitest (builder officiel ou plugin AnalogJS),
désinstaller Karma/Jasmine, puis traduire tes spies avec la table d'équivalences. Le TestBed ne bouge pas, l'API de
test est quasi identique, et tu y gagnes une vitesse et une expérience de développement sans commune mesure.
Pour aller plus loin :
- La mise en place complète en mode zoneless : Vitest + Angular zoneless
- Apprends à structurer et tester proprement tes apps Angular avec EasyAngularKit