De Karma para Vitest no Angular: a config que realmente roda

Depois de anos de karma.conf.js, um test.ts de bootstrap e um launcher de browser, a config de Vitest do Angular v21 é quase nada: uma linha no angular.json. O ng new gera ela, o build target fornece o resto, e o jsdom roda os testes. Aqui está o que essa linha te dá, o que você adiciona, e o detalhe que explica por que o fakeAsync quebra.

O test target inteiro é uma linha

Depois do Karma você espera um arquivo de config. Não existe. O test target inteiro de Vitest no angular.json é a linha do builder: { "builder": "@angular/build:unit-test" }. Sem vitest.config.ts, sem karma.conf.js, sem test.ts de bootstrap pra manter.

O ng new no Angular v21 gera exatamente isso, mais um tsconfig.spec.json cuja única linha notável é "types": ["vitest/globals"]. É o que faz describe, it e expect funcionarem sem import. O runner usa jsdom por padrão. Esse é o setup inteiro.

Algumas docs mostram um target avançado carregando um providersFile. Isso é opcional, não o default. Comece pela linha única e adicione knobs só quando um teste precisar.

O primeiro spec, na config padrãots
// O tsconfig.spec.json define "types": ["vitest/globals"], então describe, it
// e expect não precisam de import. O builder roda isso em jsdom por padrão.
describe('AppComponent', () => {
  it('cria', () => {
    const fixture = TestBed.createComponent(AppComponent);
    expect(fixture.componentInstance).toBeTruthy();
  });
});

De onde vêm polyfills, styles e assets

A razão de o test target poder ser uma linha é que ele não redefine seu app. Ele reusa o build target do projeto (a configuração de development) para compilar o app sob teste. Então os polyfills, styles e assets que você define no build já estão valendo. Você não os repete no test.

Esse detalhe tem uma consequência que vale saber antes de escrever um único teste. Como os polyfills de build incluem zone.js, o zone.js é carregado nos testes também. Mas o runner do Vitest nunca envolve cada teste na zone de teste do Angular, então fakeAsync, tick e waitForAsync lançam o erro de ProxyZone. A config está certa. Os helpers de tempo é que precisam de reescrita, um trabalho separado.

PreocupaçãoOnde se configura
Test runner, jsdom, tsconfig.spec.jsonO target test (@angular/build:unit-test)
polyfills (zone.js), styles, assetsO target build (development), reusado automaticamente
Providers globais, coverage, browser realOpções opt-in no test (abaixo)

O install: adicionar Vitest, tirar Karma

Migrar um projeto Karma existente é, na maior parte, cirurgia de dependência. Adiciona Vitest e jsdom, remove os pacotes de Karma e Jasmine, e aponta o target test pro builder de unit-test. O Angular 22 traz um schematic migrate-karma-to-vitest que automatiza a troca; os passos abaixo são o que ele faz por baixo, e o que revisar quando termina.

A primeira rodada é ng test. No CI, adicione --no-watch, já que o watch mode liga por padrão num terminal e desliga fora dele. Rode os specs simples primeiro e confirme que passam antes de tocar em qualquer coisa que mexa com tempo.

De um projeto Karma a uma rodada Vitest verdebash
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

As opções que valem adicionar

Mantenha o target mínimo e adicione só o que um teste de fato precisa. Essas são as que mais aparecem, todas em architect.test.options para você não redigitar a cada rodada.

NecessidadeOpção em test.options
Providers globais para todo teste (ex.: provideHttpClientTesting)providersFile (o default export é um Provider[])
Setup que roda depois dos polyfills e do TestBedsetupFiles
Coveragecoverage: true mais coverageReporters
Um browser real em vez de jsdombrowsers (precisa de @vitest/browser-playwright)
Sua própria config de Vitest para pluginsrunnerConfig (o Angular carrega o arquivo mas não dá suporte ao conteúdo)

A config é a parte fácil

Se o target é uma linha, por que a migração leva uma tarde? Porque a config nunca foi o trabalho. O trabalho são os specs que quebram quando a test zone some: os testes de fakeAsync e waitForAsync que precisam de uma reescrita para fake timers do Vitest. E antes de começar, vale decidir se a migração compensa para uma suíte específica.

A config te dá um runner verde em uma linha. O resto do trabalho é manter ele verde.

Artefato para reaproveitar

Primeira rodada Vitest verde, vindo do Karma

  • Adicione vitest, jsdom e @vitest/coverage-v8 como devDependencies.
  • Remova os pacotes de Karma e Jasmine (karma*, jasmine-core, @types/jasmine).
  • Deixe o target test como uma linha de builder: @angular/build:unit-test.
  • Confirme que o tsconfig.spec.json tem types: ["vitest/globals"], para describe/it/expect não precisarem de import.
  • Rode ng test --no-watch e deixe os specs simples verdes antes de tocar nos testes de fakeAsync.
  • Adicione providersFile, coverage ou browsers só quando um teste de fato precisar.

Perguntas frequentes

Preciso de um arquivo vitest.config.ts num projeto Angular?

Não, não para o setup padrão. O builder @angular/build:unit-test já traz defaults que funcionam (ambiente jsdom e tsconfig.spec.json). Você só adiciona uma config de Vitest pela opção runnerConfig quando precisa de plugins customizados, e o Angular carrega esse arquivo sem dar suporte ao conteúdo dele.

Onde coloco providers globais de teste como provideHttpClientTesting?

Num providersFile: um arquivo TypeScript cujo default export é um Provider[]. Aponte o test target para ele com a opção providersFile e ele vale para todo teste, o mesmo papel que o antigo test.ts fazia para setup global.

O runner Vitest do Angular usa um browser real?

Não, ele roda em jsdom por padrão. Adicione a opção browsers (que precisa de @vitest/browser-playwright) para rodar num browser headless real quando um teste precisar de layout real ou de uma API de browser que o jsdom não implementa.

Por que o zone.js ainda está carregado se eu migrei para Vitest?

Porque o build de teste reusa os polyfills do seu target build, e o zone.js está neles. É também por isso que o fakeAsync lança o erro de ProxyZone no Vitest: o zone.js está presente, mas o runner não estabelece a test zone que esses helpers precisam.

Como consigo coverage com o runner Vitest?

Defina coverage: true e coverageReporters (por exemplo text e lcov) nas opções de teste, ou passe --coverage. Ele usa o @vitest/coverage-v8, que você adiciona como devDependency.

Fontes consultadas

Modern Angular Playbook

Este artigo é um play.

O Playbook do Angular Moderno reúne o diagnóstico, a matriz de adoção, onze jogadas e o plano de 30 dias. Grátis, em inglês e português.

Você recebe os dois PDFs por email, pela lista do Dojo IA.

Abrir playbook →

André Ramosdisponível para vagas remotas, UTC−3Entre em contato →

Leia também

Guia · 6 min

Quando migrar uma suíte Angular para Vitest

Vitest virou o test runner padrão do Angular CLI, mas migrar uma suíte existente ainda é experimental: quais specs migrar primeiro, o que o fakeAsync quebra, e como manter o CI confiável.

Ler artigo →

Guia · 9 min

fakeAsync e o erro ProxyZone: consertando testes Angular no Vitest

Por que fakeAsync, tick e waitForAsync lançam o erro ProxyZone no runner Vitest do Angular, e as duas correções: o patch experimental de zone testing, ou a reescrita para fake timers do Vitest.

Ler artigo →

Guia · 7 min

Escolhendo o test runner do Angular em 2026: Vitest, Jest ou Karma

Um mapa de decisão pra pergunta do test runner no Angular, por onde a sua suíte realmente começa, e por que 'Cypress vs Vitest' é a pergunta errada.

Ler artigo →