---
title: "Karma to Vitest in Angular: The Config That Actually Runs"
description: "What the Angular v21 Vitest test target actually is (one line), where polyfills and styles come from, the install that drops Karma, and the few options worth adding."
deck: "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](https://angular.dev/guide/testing), 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."
author: "André Ramos"
url: "https://andreramos.dev/angular/karma-to-vitest-angular-config/"
lang: "en"
type: "article"
---

# 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](https://angular.dev/guide/testing), 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.

*The first spec, on the default config*

```ts
// 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](/angular/angular-vitest-fakeasync-proxyzone-error).

| 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.

*From a Karma project to a green Vitest run*

```bash
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-watch
```

## The 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](/angular/angular-vitest-fakeasync-proxyzone-error). And before you start, it is worth deciding [whether the migration is worth it at all](/angular/move-angular-test-suite-to-vitest) 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-v8` as devDependencies.
- Remove the Karma and Jasmine packages (`karma*`, `jasmine-core`, `@types/jasmine`).
- Set the `test` target to a single builder line: `@angular/build:unit-test`.
- Confirm `tsconfig.spec.json` has `types: ["vitest/globals"]`, so `describe`/`it`/`expect` need no import.
- Run `ng test --no-watch` and get the simple specs green before touching `fakeAsync` tests.
- Add `providersFile`, `coverage`, or `browsers` only 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

## Related

- [When to Move an Angular Test Suite to Vitest](https://andreramos.dev/angular/move-angular-test-suite-to-vitest/)
- [fakeAsync and the ProxyZone Error: Fixing Angular Tests Under Vitest](https://andreramos.dev/angular/angular-vitest-fakeasync-proxyzone-error/)
- [Choosing an Angular Test Runner in 2026: Vitest, Jest, or Karma](https://andreramos.dev/angular/choosing-an-angular-test-runner/)
