Update, September 2026: read this first if you are on a recent Angular. Angular 21 ships @angular/build:unit-test with Vitest as the default runner, and the Angular team publishes a migration guide at https://angular.dev/guide/testing/migrating-to-vitest. On Angular 20 or 21, use that builder instead of the hand-rolled setup below: you do not need the Analog plugin, a vitest.config.ts, or your own test-setup file. Everything from Step 1 through Step 5 here is the Angular 19 and earlier recipe, written in May 2025. The Jest to Vitest API notes further down apply either way.
Jest was costing me real time. On MailHive, the SaaS product I build solo, the Angular front end's full suite took well over a minute locally and several minutes in GitLab CI. Every merge request had a multi-minute tax just to see green. I switched to Vitest and cut both to a fraction. Here is exactly what I did.
Why Jest Gets Slow
Jest was designed for CommonJS and relies on Babel or ts-jest to transpile TypeScript before running anything. That transformation happens on every cold run. Vitest is built on Vite, which uses esbuild for transforms and native ESM. It also runs spec files in parallel by default (child processes since Vitest 2, worker threads if you opt in) and maintains Vite's module graph to run only tests affected by your changes in watch mode. The difference is structural, not cosmetic.
Step 1: Swap the Dependencies
# Remove Jest
npm uninstall jest jest-environment-jsdom @types/jest ts-jest jest-preset-angular
# Install Vitest + Angular plugin
npm install --save-dev vitest @vitest/coverage-v8 jsdom @analogjs/vite-plugin-angularThe @analogjs/vite-plugin-angular package handles Angular compilation inside Vite. Without it, Angular's template compiler and decorators will not work in the Vitest environment.
Step 2: vitest.config.ts
import { defineConfig } from 'vitest/config';
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',
reporter: ['text', 'lcov'],
include: ['src/app/**/*.ts'],
exclude: ['src/app/**/*.spec.ts', 'src/app/**/*.module.ts']
}
}
});globals: true is important. It makes describe, it, expect, vi available without imports, matching the Jest API your existing tests expect.
Step 3: Replace the Test Setup File
Delete setup-jest.ts or src/test.ts. Replace with a fresh src/test-setup.ts that bootstraps Angular's TestBed.
// src/test-setup.ts
import '@angular/compiler';
import 'zone.js';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);One version note on that file: @angular/platform-browser-dynamic/testing was the right import in May 2025 and was superseded from Angular 20. On Angular 20 and up, take BrowserTestingModule and platformBrowserTesting from @angular/platform-browser/testing instead. The initTestEnvironment call is otherwise identical.
Step 4: Update tsconfig.spec.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": ["vitest/globals"]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}Remove @types/jest from types if it is still there. Having both causes type conflicts.
Step 5: Update package.json Scripts
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}The Migration Tax: Jest API to Vitest
The actual work of migration is a global find-and-replace in your spec files. The APIs are nearly identical by design.
- jest.fn() becomes vi.fn(). Do a project-wide replace of 'jest.' to 'vi.'
- jest.mock() becomes vi.mock(). The hoisting behavior is slightly different, see the Vitest docs on hoisting if you use factory functions
- Snapshots mostly carry over. Vitest writes a different file header and formats a few cases differently (error snapshots, custom message separators), so expect some diffs; regenerating them once after the migration is the simplest fix
- @testing-library/jest-dom matchers need the vitest import: import '@testing-library/jest-dom/vitest'
- Zone.js still needs to be imported in your setup file. The Angular TestBed expects it
- fakeAsync, tick, and TestBed are Angular, not Jest. They work unchanged
What It Looked Like on a Real Project
The project was MailHive's Angular front end: mostly service tests with a good share of component tests using TestBed, all of it gating merges in GitLab CI behind a coverage threshold.
- Jest with ts-jest (cold): well over a minute locally
- Vitest (cold): a matter of seconds
- Vitest (warm cache): near-instant re-runs in watch mode
- GitLab CI shared runner, Jest: several minutes per pipeline
- GitLab CI shared runner, Vitest: a fraction of that, fast enough that the test job stopped being the long pole
That CI improvement is not just a number. It means feedback arrives before you switch context. Merge requests move faster. Even solo, you stop dreading the test job.
One Gotcha Worth Calling Out
If any tests still spy on Date or Math.random with jest.spyOn, for example in a shared test helper that is not a spec file and escaped the find-and-replace, they will fail because there is no jest global in Vitest. Replace with vi.spyOn. Also, vi.useFakeTimers() works the same as jest.useFakeTimers() but you need to call vi.useRealTimers() in afterEach if you are not already doing it. I had a few tests that started failing intermittently until I tracked this down.
Fast tests change behavior. When the feedback loop is instant, developers run tests before committing. When it takes minutes, they push and hope.
Budget an afternoon for a typical Angular project. Most of that is the API find-and-replace and chasing down a handful of edge cases. The payoff compounds on every single test run from that point forward.
References
- Vitest documentation. https://vitest.dev/guide/
- Analog vite-plugin-angular. https://analogjs.org/docs/packages/vite-plugin-angular