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 years, the “default Angular testing stack” meant Jasmine + Karma: Jasmine provided the test API (describe/it/expect), while Karma acted as the runner that bundled your code and executed tests inside a real browser. That approach worked well historically, but it also accumulated friction: launching browsers is comparatively heavy, and keeping browsers available and consistent is a recurring pain point in CI environments.
If you visit Karma’s official GitHub page, you can read that Karma is now deprecated and does not accept new features or general bug fixes anymore.
At the same time, the wider JavaScript ecosystem has shifted towards faster, more developer-friendly tooling. Vitest runs tests in a Node.js process using a DOM emulation layer (typically happy-dom or jsdom), so you can test DOM-oriented code without launching a browser.
In late 2025, the Angular team announced that Vitest is the new default test runner for new Angular projects, and that Vitest support in the CLI is now considered “stable and production-ready”.
Long story short: Karma is deprecated and Vitest is the future.
More on Vitest
As already mentioned, Vitest typically runs in a DOM emulation layer. happy-dom is known for its performance, while jsdom is known for its robustness and its ability to reflect the real browser API more closely. It is still possible that certain browser APIs are not fully implemented, and you may have to add or adjust mock functions for those in your tests.
It’s worth mentioning that Vitest can still run in a real browser when you need it, via browser mode (e.g., using a Playwright-backed provider). In Angular’s migration guide, browser mode is positioned as an optional step: you install a provider (such as @vitest/browser-playwright) and set a browsers option in angular.json.
The Angular team’s own “why” (as stated publicly) is essentially this: modernize testing, reduce heavyweight browser coupling when it’s not required, and provide a better developer experience—without losing the ability to do real-browser testing when it is required.
Before starting the migration
Note: The migration experience is still evolving. The Angular docs describe migrating an existing project to Vitest as experimental and tie it to the newer application build system. In practice, that means you should plan the migration like any other tooling change: iteratively, with a safety net.
Confirm you’re using the application build system
Angular’s official migration guide notes that migrating to Vitest requires the application build system, which is the default for newly created projects. Existing projects can migrate to that build system (optionally) via the documented build-system migration process.
This matters because the Vitest-based unit testing integration is implemented through the newer Angular build tooling and builders.
Decide on the migration strategy
Because Angular still supports Karma/Jasmine, you can choose between:
Big-bang migration: convert everything to Vitest, delete Karma, done.
Progressive migration: keep Karma running while you introduce Vitest, migrate test suites over time, then remove Karma once the Vitest suite is consistently green.
Progressive migration is often lower risk in real organizations, particularly for large or compliance-sensitive codebases, because it keeps a known-good runner available during refactors.
If you decide to take this strategy, you have to keep the Karma npm dependencies and the config files. The config files have to be adjusted to include only the required .spec.ts files so they do not overlap with the Vite configs. Coverage reports also have to be handled separately in this case for Karma and Vitest.
Check your current behavior
Before touching the configuration, note any Karma-specific features you rely on—custom reporters, coverage thresholds, browser launchers, or polyfills injected via the Karma builder. The Angular migration guide explicitly warns that custom karma.conf.js settings are not automatically migrated, so you should audit them before deletion.
Migration steps for Angular CLI workspaces
This section generalizes the required steps to fully migrate from Karma to Vitest, which works across typical Angular CLI applications. Read through the steps carefully, and adjust them to meet your project-specific requirements if needed.
Update Angular
An update to the latest version is recommended; otherwise, the migration might get stuck on different issues. Based on the official Angular guide:
- Run
ng update @angular/core@21 @angular/cli@21— do not use--forcewhile installing npm packages. - Install version
^21.0.0of other third-party Angular libs if possible (e.g.@ng-select/ng-select,ngx-markdown, etc.). - Run the build, the linter, and existing unit tests to confirm everything works as before. Manually opening the running application is also recommended.
Remove Karma
If you go with the full migration strategy, remove typical Karma-related files and dependencies.
Uninstall typical 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: Compare this with your own dependencies, extend it if needed, and execute it selectively.
Remove related files and their references from:
src/karma.conf.jssrc/test.tstsconfig.spec.json
Remove "karma" from compilerOptions.types in any tsconfig.json or tsconfig.spec.json files.
Install dependencies and adjust configs
Install new dependencies:
npm install --save-dev vitest@^4.0.0 jsdom @vitest/coverage-istanbul
Explanation:
vitest(at least version 4 is recommended; otherwise, you might run into issues)jsdomorhappy-domaccording to your choice- Custom coverage reporter if needed (Istanbul in my case)
Add @angular/build:unit-test as the new builder for your unit tests by adjusting angular.json:
{
"projects": {
"your-project-name": {
"architect": {
"test": {
"builder": "@angular/build:unit-test",
"options": {
"tsConfig": "tsconfig.spec.json"
}
}
}
}
}
}
Add "vitest/globals" to compilerOptions.types in tsconfig.json.
Change tsconfig.spec.json to include polyfills.ts, either in the files array or in the include array.
If you used the --code-coverage flag while running your unit tests, it has to be replaced with --coverage due to API changes in ng test. Similarly, the --browsers option has to be removed as long as you rely on jsdom or happy-dom.
From this point, we are set up and can execute ng test and run Vitest. Of course, it is going to run into several issues, as we haven’t migrated the tests themselves yet. That is something we are going to fix in the next chapters. But before that, we’re going to extend the test.options object with a "runnerConfig": "vitest-ng.config.ts" option to use a custom Vitest config.
Use a custom Vitest config
Vitest projects typically contain a vitest.config.ts file at the root of the project, which tells Vitest how to run the tests—which files are included, where the setup files are, which coverage reporter should be used, etc.
As we have seen, Angular runs Vitest without such a file, which also causes some issues when it comes to third-party tools, like using Vitest IDE extensions to run and debug your tests directly from the editor. In this case, the extensions are looking for a vitest.config.ts file, but since they do not find one, they are clueless and fail to run our Angular tests. This is a known issue in the community, and Younes Jaaidi already addressed the problem in this Angular GitHub issue:
https://github.com/angular/angular-cli/issues/31734
The good news is that we are going to implement a solution for it right here.
First, Angular actually provides a "runnerConfig" option, which makes it possible to feed the builder with a standard Vitest config file.
A typical vitest.config.ts file in the root would look something 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);
You can see that we define how the paths should be resolved, which environment (jsdom) we are using, how coverage should be configured, and what setup files we need.
Note: test-setup.ts should look something like this to initialize the Angular test environment:
import { TestBed } from '@angular/core/testing';
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
This solution makes our config compatible with typical editor plugins and makes it possible to run and debug our unit tests granularly directly in the editor.
However, if we feed this config to our Angular builder, it is going to complain that testEnvironment has already been called.
We want to keep vitest.config.ts as the single source of truth, so we create an extended version—let’s call it vitest-ng.config.ts—and override the setupFiles like this:
import { defineConfig } from 'vitest/config';
import baseConfig from './vitest.config';
export default defineConfig({
...baseConfig,
test: {
...baseConfig.test,
setupFiles: []
}
});
Now we can feed this into Angular by adjusting our angular.json:
{
"projects": {
"your-project-name": {
"architect": {
"test": {
"builder": "@angular/build:unit-test",
"options": {
"tsConfig": "tsconfig.spec.json",
"runnerConfig": "vitest-ng.config.ts"
}
}
}
}
}
}
Now we can edit vitest.config.ts whenever we want to adjust general settings, and it is going to be consumed by our editor plugins and ng test as well.
Migrate existing test files
Angular provides a migration script called refactor-jasmine-vitest, which is being improved over time, but according to the official migration guide, it is still experimental, meaning we cannot expect it to perform a 100% complete migration. I’m going to go through the issues I found after running the script and also give some advice on how to get past them.
First, before running the script, make sure your repository is in a clean, committed state.
Then execute the script:
npx ng g @schematics/angular:refactor-jasmine-vitest
I also advise committing all the changes to preserve them.
After running the migration, I found that it ignored a few syntax changes (e.g. toBeTrue does not exist anymore; toBeTruthy is the successor), it did not get rid of waitForAsync and fakeAsync methods (zone.js), and it had issues with files that had a .spec.ts extension but did not contain any tests.
The most efficient way to solve the remaining issues in batch was to select an AI agent tool (like Copilot in VS Code), select a decent model (GPT-5.3-Codex at this time), and execute a few detailed prompts that pointed out the issues clearly.
Modern Angular
More about Signal Forms can be found in my new book Modern Angular - Architecture, Concepts, Implementation. This book covers everything you need for building modern business applications with Angular: from Signals and state patterns to architecture, AI assistants, testing, and practical solutions for real-world projects.
Prompts
- Search for all the
.toBe,.toBeTruthy, and.toBeFalsymethods where they get 2 params, and move the 2nd param to the previousexpectmethod as a second param in all.spec.tsfiles. - Wherever I use
waitForAsyncandTestBed.configureTestingModule, change thewaitForAsyncto a simpleasyncmethod instead and await theTestBed.configureTestingModule. - Rename all the
.spec.tsfiles to.tsfiles that do not contain a test suite, and move them tosrc/app/test-utils/**. - Remove empty spec files.
- Find
.spec.tsfiles that usefakeAsyncand avoid using it due to migration to Vitest.
Important:
- Execute the prompts one by one.
- Always review all the changes and make adjustments (or even revert) if needed.
- Committing changes between prompts is also helpful.
Inserting this extra note helps a lot:
ng test --watch=false
to check the results and iterate if needed.
In the end, the tests should complete, and all the code changes should be understood.
(Optional): Run unit tests in the browser
At this point, you should already be able to run all your unit tests using Vitest. 🎉
However, there is still some room for improvement when it comes to the test environment and performance.
As long as your Vitest setup relies on environments such as jsdom or happy-dom, it relies on lightweight DOM implementations. They are easy to set up and work well for most test cases, but they are still simulations, not real browsers. An alternative approach is to run your unit tests inside a real browser engine using Playwright. Vitest provides first-class support for this through the @vitest/browser-playwright package. This allows tests to run in a fully implemented browser environment (for example, Chromium), which can improve both compatibility and performance.
I ran a quick benchmark on a test project:
- 125 test files
- 619 tests total
Average pure test execution time:
jsdom: 23.31 seconds- Playwright (headless Chromium): 16.83 seconds
This resulted in roughly a 28% performance improvement, which is quite significant for larger test suites.
In order to switch from jsdom to Playwright’s Chromium, you have to do the following:
First, install the Playwright integration for Vitest:
npm install --save-dev @vitest/browser-playwright
Then update 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, and the environment behaves much closer to how Angular code runs in production. You may also see noticeable performance improvements.
Notes for Nx Workspaces
Migration from Karma to Vitest works in Nx monorepos more or less similarly to how it works in Angular CLI projects, with a few differences.
The biggest difference is that the migration script provided by Angular—refactor-jasmine-vitest—won’t work due to the different project structure. In practice, that means that the list of prompts provided previously has to be extended with a few extra steps according to the thrown errors, mostly for handling syntax changes.
The next difference is that an Nx workspace doesn’t have a single angular.json file responsible for all of your projects (apps and libs), but instead defines a project.json file for each of the projects you have, which defines the test executor as well. Here, in project.json, you can set test.executor to angular/build:unit-test in order to use the Vitest runner provided by Angular. Also, in test.options.runnerConfig, you can provide an Angular- and project-specific vitest-ng.config.ts file to pass your configs to the runner.
It’s important not to place vitest.config.ts files at project level, as Vitest plugins and extensions are going to automatically detect them, and they are not very good at resolving the path to project-specific tests. So the working version was, in terms of extension compatibility, to put a single vitest.config.ts file at the root of the Nx workspace, with the same settings as previously mentioned, listing setupFiles with a global test-setup.ts which calls initTestEnvironment for Angular compatibility.
Conclusion
For many years, Karma + Jasmine served Angular developers well. However, the ecosystem has evolved. With Karma now officially deprecated and Angular CLI adopting Vitest as the default testing solution for new projects, the direction of the platform is clear.
Migrating from Karma to Vitest is more than just swapping one test runner for another, but the migration process is manageable when approached methodically:
- First, ensure the project runs on a modern Angular version and build system.
- Replace the Karma infrastructure with Vitest dependencies and builder configuration.
- Introduce a custom Vitest configuration so both Angular and editor tooling can work seamlessly.
- Gradually refactor existing Jasmine-based tests to Vitest-compatible syntax.
- Use automation tools, migration scripts, or AI-assisted prompts to handle repetitive refactoring tasks efficiently.
Once the migration is complete, the benefits become clear: performance, continuous updates, and better IDE integration. These improvements make the testing experience noticeably smoother for Angular developers.
It’s also worth noting that the ecosystem around Vitest continues to evolve quickly. Tooling support, editor integrations, and Angular-specific utilities are improving steadily. This means that migrating today not only solves the Karma deprecation issue, but also positions your project to take advantage of future improvements in the Angular testing landscape.
In short: Vitest is not just a replacement for Karma—it is the foundation for Angular’s next generation of testing workflows.
Resources
- 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 is a frontend architect and freelance consultant in the DACH region, with deep expertise in Angular and modern web architectures. He focuses on advancing developer workflows and applying LLMs and agentic systems to real-world frontend solutions.
