Contents
Why Vitest instead of Karma?
More on Vitest
Before starting the migration
Confirm you’re using the application build system
Decide on the migration strategy
Check your current behavior
Migration steps for Angular CLI workspaces
Updating Angular
Remove Karma
Install dependencies and adjust configs
Use a custom Vitest config
Migrate existing test files
Prompts
(Optional): Run unit tests in the browser
Notes for Nx Workspaces
Conclusion
Resources
Why Vitest instead of Karma?
For a long time, the conventional Angular testing setup consisted of Jasmine paired with Karma. Jasmine supplied the testing API (describe/it/expect), while Karma handled the role of the runner, bundling the code and executing the test suite inside an actual browser. This combination functioned well in the past, but it inevitably introduced certain inconveniences: spinning up browsers is comparatively slow, and maintaining consistent browser availability is a frequent headache in CI setups.
A quick visit to Karma's official GitHub page reveals that the project is now deprecated. It no longer accepts new features, and general bug fixes are also discontinued.
Meanwhile, the broader JavaScript ecosystem has moved towards quicker, more developer-centric tooling. Vitest executes tests in a Node.js process, relying on a DOM emulation layer—typically happy-dom or jsdom—to test DOM-oriented code without requiring a browser instance.
In late 2025, the Angular team made an announcement: Vitest would be the default test runner for new Angular projects, and the CLI's support for Vitest was officially marked as "stable and production-ready".
To summarize: Karma is deprecated, and Vitest represents the future.
More on Vitest
As mentioned earlier, Vitest generally operates within a DOM emulation layer. happy-dom is praised for its speed, whereas jsdom is recognized for its reliability and its closer approximation of the actual browser APIs. However, it's still possible that certain browser APIs are not fully implemented, so you might need to introduce or modify mock functions to compensate in your test files.
It's also worth noting that Vitest isn't limited to DOM emulation. Through its browser mode—say, with a Playwright-backed provider—it can execute tests in a real browser when necessary. According to Angular's migration guide, this is an optional step that involves installing a provider such as @vitest/browser-playwright and configuring a browsers property in angular.json.
The Angular team's publicly stated rationale is straightforward: they want to modernize testing, reduce the heavy dependency on browsers when it's not strictly necessary, and offer a more pleasant development experience—all while retaining the ability to perform genuine browser-based testing when the situation demands it.
Before starting the migration
Note: The migration process is still a work in progress. The Angular docs describe migrating an existing project to Vitest as experimental and link it to the newer application build system. In practice, this means you should treat the migration as you would any other tooling change: proceed incrementally and ensure you have a safety net in place.
Confirm you're using the application build system
The official Angular migration guide points out that transitioning to Vitest requires utilizing the application build system, a default for freshly generated projects. For older projects, a documented migration path exists (listed as optional) to switch to this new build system.
This is a crucial detail because the Vitest-based unit testing integration is built upon the newer Angular tooling and its builders.
Decide on the migration strategy
Given that Angular still offers support for Karma/Jasmine, you have two main strategic options:
Big-bang migration: convert all tests to Vitest in one go, remove Karma entirely, and finish.
Progressive migration: maintain Karma as a running system while you gradually introduce Vitest, migrating test suites incrementally. Only once the Vitest suite is consistently passing would you then remove Karma.
The progressive approach is often less risky for real-world organizations, especially for large codebases or those with strict compliance requirements, since it keeps a proven test runner available during the transition period.
If you opt for this strategy, you'll need to keep the Karma npm packages and its configuration files. However, you'll need to adjust those config files to include only the specific .spec.ts files required, ensuring they don't conflict with the Vite configuration. Additionally, in this hybrid state, coverage reporting must be managed independently for both Karma and Vitest.
Check your current behavior
Before you start modifying any configuration, audit which Karma-specific features your current setup depends on. This includes custom reporters, coverage thresholds, browser launchers, or polyfills that are injected through the Karma builder. The Angular migration guide explicitly cautions that custom settings within karma.conf.js won't be migrated automatically, so it's essential to review and catalog them before removing anything.
Moving an Angular CLI workspace to Vitest
The steps below outline a general path for migrating Angular CLI projects from Karma to Vitest. They should work for most projects, though you may need to adapt them to your specific setup.
Bring Angular up to date
It’s best to start with the newest Angular version available; otherwise, you may encounter unrelated errors that complicate the migration. Following the official Angular guidance:
- Execute
ng update @angular/core@21 @angular/cli@21— avoid the--forceflag when installing packages. - Update any additional Angular libraries (for instance,
@ng-select/ng-select,ngx-markdown) to version^21.0.0where feasible. - Verify the build, linting, and existing unit tests all pass as before. It’s also a good idea to manually check the running app.
Strip out Karma
For a complete migration, you’ll need to delete the usual Karma-related packages and files.
Remove the standard dependencies:
npm uninstall karma karma-chrome-launcher karma-coverage-istanbul-reporter karma-jasmine karma-jasmine-html-reporter karma-junit-reporter jasmine-core jasmine-spec-reporter karma-coverage @types/jasmine @types/jasminewd2 --save
Note: Check this list against your own project, add anything extra you have, and run the commands selectively.
Delete the related files and their references from:
src/karma.conf.jssrc/test.tstsconfig.spec.json
In any tsconfig.json or tsconfig.spec.json, drop "karma" from the compilerOptions.types array.
Set up new dependencies and configurations
Add the new packages:
npm install --save-dev vitest@^4.0.0 jsdom @vitest/coverage-istanbul
What each one is for:
vitest(version 4 or higher is recommended to avoid compatibility problems)jsdomorhappy-dom, depending on your preference- A custom coverage reporter if you need one (I used Istanbul)
Switch the test builder for unit tests to @angular/build:unit-test by modifying angular.json:
{
"projects": {
"your-project-name": {
"architect": {
"test": {
"builder": "@angular/build:unit-test",
"options": {
"tsConfig": "tsconfig.spec.json"
}
}
}
}
}
}
In tsconfig.json, add "vitest/globals" to compilerOptions.types.
Update tsconfig.spec.json so that polyfills.ts is included, either via the files or the include entry.
If you’ve been using the --code-coverage option when calling ng test, swap it for --coverage — the API changed. Likewise, remove the --browsers option entirely, since jsdom or happy-dom replaces the need for it.
At this stage, everything is ready to try ng test and see Vitest in action. It will almost certainly fail, because the test files themselves haven’t been converted yet — that’s covered in the next sections. Before we get to that, though, let’s add a "runnerConfig": "vitest-ng.config.ts" entry to the test.options so we can use a custom Vitest configuration.
Work with a custom Vitest config
Most Vitest setups include a vitest.config.ts file at the project root. This file tells Vitest which test files to look for, where setup files live, which coverage reporter to use, and similar details.
As it turns out, Angular runs Vitest without such a file. That causes problems with third-party tooling — for instance, Vitest IDE extensions that let you run or debug tests directly in your editor. These extensions look for a vitest.config.ts file; when they don’t find one, they have no idea what to do with Angular tests. This is a known issue in the community, and Younes Jaaidi raised it in this Angular GitHub issue:
https://github.com/angular/angular-cli/issues/31734
The good news is that we’re going to implement a fix for it right now.
Angular ships with a "runnerConfig" option that lets us pass a standard Vitest config file to the builder.
A standard vitest.config.ts at the root might look like this:
import path from 'node:path';
import { defineConfig, ViteUserConfig } from 'vitest/config';
export const baseConfig: ViteUserConfig = {
resolve: {
alias: {
src: path.resolve(__dirname, 'src')
}
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['@angular/localize/init', './src/app/test-utils/test-setup.ts'],
coverage: {
provider: 'istanbul',
reporter: ['lcovonly', 'html'],
reportsDirectory: './coverage'
}
}
};
export default defineConfig(baseConfig);
Notice how we set up path resolution, pick the environment (jsdom), configure coverage, and list setup files.
Note: test-setup.ts should resemble the following to get the Angular test environment initialized:
import { TestBed } from '@angular/core/testing';
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
This approach makes the config visible to typical editor plugins, allowing you to run and debug individual tests straight from the IDE.
However, when this config is fed to the Angular builder, it errors out with a complaint that testEnvironment has already been called.
To keep vitest.config.ts as the canonical config, we create a derived version, say vitest-ng.config.ts, and override the setupFiles separately:
import { defineConfig } from 'vitest/config';
import baseConfig from './vitest.config';
export default defineConfig({
...baseConfig,
test: {
...baseConfig.test,
setupFiles: []
}
});
We then point Angular to this new file by updating angular.json:
{
"projects": {
"your-project-name": {
"architect": {
"test": {
"builder": "@angular/build:unit-test",
"options": {
"tsConfig": "tsconfig.spec.json",
"runnerConfig": "vitest-ng.config.ts"
}
}
}
}
}
}
From now on, any changes you make to vitest.config.ts — whether it’s for editor plugins or for ng test — will be picked up automatically.
Convert the existing test files
Angular provides a migration script called refactor-jasmine-vitest that is still under active development. However, the official migration guide notes that it is experimental, so we shouldn’t expect it to handle everything perfectly. Below, I’ll walk through the problems I encountered after running it and share some tips on working around them.
Before running the script, make sure your repository is clean and all changes are committed.
Then, run the script:
npx ng g @schematics/angular:refactor-jasmine-vitest
It’s a good idea to commit the results right after, so you don’t lose them.
Looking at the migration output, I noticed it missed several syntax changes (e.g., it kept toBeTrue, which has been replaced by toBeTruthy), it left behind waitForAsync and fakeAsync calls (both from zone.js), and it also had trouble with .spec.ts files that didn’t actually contain any tests.
To clean up the remaining issues in bulk, I found it most practical to use an AI assistant (such as Copilot in VS Code), pick a capable model (GPT-5.3-Codex at the time of writing), and give it clear, detailed instructions about each problem.
Modern Angular
If you’d like to dig deeper into Signal Forms, check out my new book Modern Angular - Architecture, Concepts, Implementation. It has everything you need to build modern Angular business applications: Signals and state patterns, architecture, AI assistants, testing, and practical solutions for real projects.
Prompts
- Go through every
.spec.tsfile and look for calls to.toBe,.toBeTruthy, or.toBeFalsythat receive 2 arguments; move the second argument up to the correspondingexpectcall as an additional parameter. - In places where
waitForAsyncis used withTestBed.configureTestingModule, replacewaitForAsyncwith a plainasyncfunction andawaittheTestBed.configureTestingModulecall. - For all
.spec.tsfiles that don’t contain a test suite, rename them to.tsand relocate them tosrc/app/test-utils/**. - Delete any empty spec files.
- Review
.spec.tsfiles that usefakeAsyncand find ways to avoid it, since the migration to Vitest removes the need for it.
Important:
- Run the prompts one at a time.
- Always review the resulting changes and tweak or revert anything that looks wrong.
- It’s also helpful to commit after each prompt.
Including this extra note below helps:
ng test --watch=false
to go over the output and make adjustments as needed.
In the end, all tests should pass, and you should understand every code change that was made.
(Optional): Run unit tests in the browser
By now, you should have a working Vitest setup for all your unit tests. 🎉
Still, there’s room to improve the test environment and performance.
When your Vitest configuration depends on environments like jsdom or happy-dom, you’re relying on lightweight DOM implementations. These are simple to set up and handle most cases well, but they’re simulations rather than actual browsers. A different option is to run your unit tests in a real browser engine via Playwright. Vitest has first-class integration for this with the @vitest/browser-playwright package. This approach runs tests in a fully implemented browser environment (like Chromium), which can be both more compatible and faster.
I ran a quick benchmark on a sample project:
-
125 test files
-
619 tests total
Average time for the tests alone:
jsdom: 23.31 seconds- Playwright (headless Chromium): 16.83 seconds
That’s roughly a 28% improvement in performance, which matters a lot for bigger test suites.
Here’s how to switch from jsdom to Playwright’s Chromium:
Start by installing the Playwright adapter for Vitest:
npm install --save-dev @vitest/browser-playwright
Then adjust the Vitest configuration in vitest.config.ts:
// Import the Playwright provider
import playwright from '@vitest/browser-playwright';
export default defineConfig({
test: {
// Replace the jsdom environment with the browser configuration.
browser: {
provider: playwright(),
enabled: true,
headless: true,
instances: [
{ browser: 'chromium' }
]
}
}
});
With this configuration, tests run inside a real headless Chromium browser, mimicking the production environment for Angular code much more closely. You’re also likely to see a significant speed boost.
Notes on Nx Workspaces
Migrating from Karma to Vitest in an Nx monorepo follows a similar pattern to Angular CLI projects, with a few key differences.
The main distinction is that Angular’s migration script, refactor-jasmine-vitest, won’t work with Nx’s structure. Practically speaking, this means you’ll need to expand the list of prompts above with extra steps to handle the syntax changes that come up.
Another difference is that Nx workspaces don’t rely on a single angular.json file for the whole project set. Instead, each app or lib has its own project.json, which also specifies the test executor. In project.json, set test.executor to angular/build:unit-test to use Angular’s Vitest runner. You can also point test.options.runnerConfig to an Angular- and project-specific vitest-ng.config.ts file to supply your configuration.
It’s important to avoid placing vitest.config.ts files at the project level. Vitest plugins and editor extensions will detect them automatically, and they tend to struggle with resolving paths to project-specific tests. The version that worked for me, in terms of editor extension compatibility, was to keep a single vitest.config.ts at the root of the Nx workspace. It uses the same settings as before, listing setupFiles with a global test-setup.ts that calls initTestEnvironment for Angular support.
Final Thoughts
Karma paired with Jasmine was the standard choice for Angular test setups over the years. But the surrounding tooling has moved on. Given Karma’s official deprecation and the Angular CLI’s shift to Vitest for newly generated projects, the path forward is well-defined.
Switching from Karma to Vitest involves more than a simple runner swap, yet the transition remains straightforward when handled step by step:
- Verify that the project is on a recent Angular release and a modern build pipeline.
- Swap out the Karma setup for Vitest packages and adjust the builder settings accordingly.
- Add a tailored Vitest configuration that keeps both the Angular build and the editor experience intact.
- Work through the existing Jasmine specs and convert them to Vitest-compatible patterns incrementally.
- Leverage automated tooling, migration scripts, or AI-assisted workflows to speed up repetitive code changes.
Once the dust settles, the gains are tangible: faster runs, steady maintenance, and tighter editor support. For Angular developers, the day-to-day testing workflow feels noticeably more responsive.
The Vitest ecosystem is also advancing rapidly. Editor plugins, supporting libraries, and Angular-specific tooling are maturing with each release. Shifting to Vitest now resolves the immediate Karma deprecation concern while also preparing the project for ongoing enhancements in Angular’s testing stack.
Ultimately, Vitest offers more than a Karma alternative—it lays the groundwork for the next generation of Angular testing practices.
Further Reading
- https://angular.dev/guide/testing/migrating-to-vitest
- https://github.com/angular/angular-cli/issues/31734
- https://github.com/karma-runner/karma
- https://nx.dev/docs/technologies/test-tools/vitest/introduction
Marcell Kiss works as a frontend architect and freelance consultant in the DACH region, specializing in Angular and modern web architectures. His focus is on improving developer workflows and applying LLMs with agentic systems to practical frontend challenges.

