Modern Angular
Karma to Vitest in Angular: The Config That Actually Runs
After years of karma.conf.js, a test.ts bootstrap, and a browser launcher, the Angular v21 Vitest config is almost nothing: one line in angular.json. ng new generates it, the build target supplies the rest, and jsdom runs the tests. Here is what that line gives you, what you add, and the one detail that explains why fakeAsync breaks.
The whole test target is one line
After Karma you expect a config file. There is not one. The entire Vitest test target in angular.json is the builder line: { "builder": "@angular/build:unit-test" }. No vitest.config.ts, no karma.conf.js, no test.ts bootstrap to maintain.
ng new on Angular v21 generates exactly that, plus a tsconfig.spec.json whose only notable line is "types": ["vitest/globals"]. That is what lets describe, it, and expect work without an import. The runner defaults to a jsdom environment. That is the whole setup.
Some docs show an advanced target carrying a providersFile. That is optional, not the default. Start with the one line and add knobs only when a test needs them.
// tsconfig.spec.json sets "types": ["vitest/globals"], so describe, it, and
// expect need no import. The unit-test builder runs this in jsdom by default.
describe('AppComponent', () => {
it('creates', () => {
const fixture = TestBed.createComponent(AppComponent);
expect(fixture.componentInstance).toBeTruthy();
});
});Where polyfills, styles, and assets come from
The reason the test target can be one line is that it does not redefine your app. It reuses your project's build target (the development configuration) to compile the app under test. So the polyfills, styles, and assets you set under build are already in effect. You do not repeat them under test.
That detail has a consequence worth knowing before you write a single test. Because the build polyfills include zone.js, zone.js is loaded in your tests too. But the Vitest runner never wraps each test in Angular's test zone, so fakeAsync, tick, and waitForAsync throw the ProxyZone error. The config is fine. The timing helpers need a rewrite, which is a separate job.
| Concern | Where it is configured |
|---|---|
Test runner, jsdom, tsconfig.spec.json | The test target (@angular/build:unit-test) |
polyfills (zone.js), styles, assets | The build target (development), reused automatically |
| Global providers, coverage, real browser | Opt-in options under test (below) |
The install: add Vitest, drop Karma
Moving an existing Karma project is mostly dependency surgery. Add Vitest and jsdom, remove the Karma and Jasmine packages, and point the test target at the unit-test builder. Angular 22 ships a migrate-karma-to-vitest schematic that automates the switch; the steps below are what it does under the hood, and what to review when it is done.
The first run is ng test. In CI add --no-watch, since watch mode defaults on in a terminal and off otherwise. Run the simple specs first and confirm they pass before you touch anything that bends time.
npm install --save-dev vitest jsdom @vitest/coverage-v8
npm rm karma karma-chrome-launcher karma-jasmine karma-jasmine-html-reporter karma-coverage jasmine-core @types/jasmine
ng test --no-watchThe options worth adding
Keep the target minimal and add only what a test actually needs. These are the ones that come up most, all set under architect.test.options so you do not retype them on every run.
| Need | Option under test.options |
|---|---|
Global providers for every test (e.g. provideHttpClientTesting) | providersFile (its default export is a Provider[]) |
| Setup that runs after polyfills and TestBed | setupFiles |
| Coverage | coverage: true plus coverageReporters |
| A real browser instead of jsdom | browsers (needs @vitest/browser-playwright) |
| Your own Vitest config for plugins | runnerConfig (Angular loads it but does not support its contents) |
The config is the easy part
If the target is one line, why does the migration take an afternoon? Because the config was never the work. The work is the specs that break when the test zone is gone: the fakeAsync and waitForAsync tests that need a rewrite to Vitest fake timers. And before you start, it is worth deciding whether the migration is worth it at all for a given suite.
The config gets you a green runner in one line. The rest of this work is about keeping it green.
Reusable artifact
First green Vitest run, from Karma
- Add
vitest,jsdom, and@vitest/coverage-v8as devDependencies. - Remove the Karma and Jasmine packages (
karma*,jasmine-core,@types/jasmine). - Set the
testtarget to a single builder line:@angular/build:unit-test. - Confirm
tsconfig.spec.jsonhastypes: ["vitest/globals"], sodescribe/it/expectneed no import. - Run
ng test --no-watchand get the simple specs green before touchingfakeAsynctests. - Add
providersFile,coverage, orbrowsersonly when a test actually needs them.
FAQ
Do I need a vitest.config.ts file in an Angular project?
No, not for the default setup. The @angular/build:unit-test builder ships working defaults (a jsdom environment and tsconfig.spec.json). You only add a Vitest config through the runnerConfig option when you need custom plugins, and Angular loads that file without supporting its contents.
Where do I put global test providers like provideHttpClientTesting?
In a providersFile: a TypeScript file whose default export is a Provider[]. Point the test target at it with the providersFile option and it applies to every test, the same role the old test.ts played for global setup.
Does the Angular Vitest runner use a real browser?
No, it runs in jsdom by default. Add the browsers option (which needs @vitest/browser-playwright) to run in a real headless browser when a test needs real layout or a browser API jsdom does not implement.
Why is zone.js still loaded if I moved to Vitest?
Because the test build reuses your build target's polyfills, and zone.js is in them. That is also why fakeAsync throws the ProxyZone error under Vitest: zone.js is present, but the runner does not establish the test zone those helpers need.
How do I get coverage with the Vitest runner?
Set coverage: true and coverageReporters (for example text and lcov) under the test options, or pass --coverage. It uses @vitest/coverage-v8, which you add as a devDependency.
Sources checked
- https://angular.dev/guide/testing
- https://angular.dev/guide/testing/migrating-to-vitest
- https://angular.dev/reference/configs/angular-compiler-options
- https://vitest.dev/guide
- https://vitest.dev/config
- https://blog.angular.dev/announcing-angular-v21-57946c34f14b
Angular Vitest Migration Kit
Migrating this suite to Vitest?
The Angular Vitest Migration Kit sizes your migration by risk, then packages the config, the fakeAsync cookbook, an AI-agent skill, and a runnable demo. The conversions in this article, done for you.
Verified on Angular 21.2.14 / Vitest 4.1.8. Includes the migration estimator, the PDF guide, copy-paste config, an AI-agent skill, and a runnable demo.
André Ramosavailable for remote roles, UTC−3Get in touch →