Coverage e CI do Vitest no Angular: providers, thresholds e o gap do Karma

O coverage é first-class no Angular CLI: o ng test --coverage lê as opções do builder no angular.json, e o coverageThresholds derruba a rodada abaixo de um piso que você define (a doc usa 80%). Mas a escolha de provider, v8 ou istanbul, não é uma opção do Angular. Ela vive direto na config do Vitest, e o Angular não dá suporte ao conteúdo desse arquivo.

Coverage é opção do builder, não arquivo de config

No Karma, coverage era o karma-coverage plugado no karma.conf.js: um bloco de reporter, um diretório de saída do istanbul, thresholds num objeto check. O builder do Vitest move tudo isso para o angular.json, em architect.test.options. Não há arquivo de config de coverage pra manter no caso comum. Você define coverage: true e as chaves que ficam junto.

A flag é ng test --coverage, e a doc é explícita que coverage é uma feature first-class do CLI. A única dependência que você adiciona é o @vitest/coverage-v8. O próprio help da flag diz o estado padrão: If not specified, the coverage configuration from a runner configuration file will be used if present. Otherwise, coverage is disabled by default. Ou seja, coverage fica desligado até você pedir, seja na flag, seja no arquivo.

Os nomes dos reporters são o conjunto istanbul que você já conhece do Karma: text, text-summary, html, lcov, lcovonly, cobertura, json, json-summary. O lcov e o cobertura são os dois que um dashboard de CI consome, então são os que vale fixar na config versionada em vez de passar na linha de comando.

Coverage no angular.json, do jeito que a doc mostratext
# angular.json -> projects.<nome>.architect.test.options
# Precisa de: npm i -D @vitest/coverage-v8
{
  "builder": "@angular/build:unit-test",
  "options": {
    "coverage": true,
    "coverageReporters": ["text-summary", "lcov", "cobertura"],
    "coverageThresholds": {
      "statements": 80,
      "branches": 80,
      "functions": 80,
      "lines": 80
    }
  }
}

O gap: a escolha de provider não é sua aqui

Esse é o único ponto em que o Angular te dá menos margem do que aparenta, e vale saber disso antes de prometer um relatório istanbul pra um time. O Vitest tem dois providers de coverage: o v8 (o padrão) coleta em runtime via node:inspector e roda seu código como está, enquanto o istanbul instrumenta os arquivos de source e roda em qualquer runtime JavaScript. Eles diferem de verdade: o v8 não consegue restringir a coleta a módulos específicos, e só o v8 exige um runtime baseado em V8.

A página de code coverage do Angular documenta exatamente um pacote de provider, o @vitest/coverage-v8. Não existe opção coverageProvider no builder. Trocar pro istanbul é um ajuste que vive só na config do Vitest, coverage.provider: 'istanbul', o que significa um vitest.config.ts alcançado pela opção runnerConfig. E o guia de migração é direto sobre esse arquivo: o time Angular não dá suporte ao conteúdo dele nem aos plugins de terceiros. Dá pra fazer; mas você sai do caminho suportado quando faz.

Minha leitura: fique no v8 a não ser que tenha um motivo concreto pra não ficar. Desde o Vitest 3.2 o provider v8 usa um remapeamento baseado em AST que a doc diz produzir relatórios idênticos aos do istanbul, então o velho argumento de precisão pra trocar praticamente sumiu. Eu só recorreria ao istanbul se precisasse da instrumentação por módulo dele, e trataria esse arquivo de runnerConfig como algo que o time mantém e revisa, não um padrão pra copiar de um blog.

Dimensãov8 (padrão do Angular)istanbul (só via runnerConfig)
Pacote@vitest/coverage-v8@vitest/coverage-istanbul
Como coletaRuntime via node:inspector, source roda como estáInstrumenta os arquivos antes de rodar
Definido emcoverage: true no angular.jsoncoverage.provider: 'istanbul' numa config do Vitest
Suporte do AngularFirst-class, documentadoConteúdo sem suporte (runnerConfig)
Restringir a módulosNãoSim
Precisão vs istanbulIdêntica desde o Vitest 3.2 (remap AST)Base

Thresholds que de fato derrubam o build

Um número de coverage que ninguém cobra é decoração. O que o coverageThresholds faz é transformar o coverage num portão: a doc do Angular diz claramente que, se o coverage cair abaixo do piso configurado quando você roda os testes, o comando falha. Esse exit diferente de zero é o que torna o portão real no CI, e é o mesmo comportamento que o bloco check do karma-coverage te dava, agora expresso no angular.json.

As quatro chaves são statements, branches, functions e lines, cada uma uma porcentagem. Eu não escolho 80 por ser um número bom; escolho o piso que a suíte já passa hoje, versiono, e subo conforme o coverage melhora. Um threshold acima de onde você está é só um build vermelho no primeiro dia, o que treina o time a passar --no-coverage e mata o propósito.

Uma ressalva específica da saída do Karma: as porcentagens de coverage mudam quando o provider ou o conjunto de arquivos incluídos muda, então o número que você cobrava no karma-coverage não é garantia do número que o Vitest reporta pro mesmo código. Meça primeiro no runner novo e só então defina o threshold a partir dessa medição.

Checagem local de coverage antes de confiar no thresholdbash
# Veja o número real que o Vitest reporta pro seu codigo, e tire o piso dele.
ng test --coverage --coverage-reporters text-summary --no-watch

# Estreite o conjunto incluido se arquivos vendados ou gerados distorcem a porcentagem.
ng test --coverage --coverage-include "src/app/**/*.ts" --no-watch

O que muda no runner do CI

A diferença mecânica de CI em relação ao Karma é que você não briga mais com um launcher de browser. O builder do Vitest roda em jsdom por padrão, então não há flag de Chrome headless pra cuidar. O que você ainda quer é rodar uma vez só e ter um relatório legível por máquina.

O run mode é quase automático. O Vitest entra em run mode quando process.env.CI está setado, e cai pra run mode quando o stdin não é um TTY, então um ng test simples na maioria dos provedores de CI já roda uma vez e sai. Mesmo assim eu passo --no-watch explícito no comando de CI. Depender da detecção de ambiente é o tipo de comportamento implícito que vira pipeline travado no dia em que você roda o mesmo comando num contexto que por acaso parece interativo.

Pro relatório, os reporters de execução incluem o junit, o formato que Jenkins, GitLab e Azure Pipelines leem nativamente pra resultado por teste. Coverage e resultado de teste são dois fluxos separados: --reporters junit é o resultado do teste, --coverage-reporters cobertura (ou lcov) é o coverage que o dashboard lê. O par --no-watch --no-progress da era Karma vira só o run mode padrão mais os reporters que você de fato precisa.

Setup no KarmaEquivalente no builder VitestObservação
karma-coverage no karma.conf.jscoverage + coverageReporters no angular.jsonSem arquivo de config de coverage no caso comum.
Reporter istanbul, fixoProvider v8; istanbul só via runnerConfigProvider não é opção first-class do Angular.
Thresholds no check do karma.conf.jscoverageThresholds (statements/branches/functions/lines)Derruba a rodada abaixo do piso, mesmo portão.
--no-watch --no-progress pro CIRun mode autodetectado; passe --no-watch assim mesmoSem launcher de browser pra configurar.
karma-junit-reporter--reporters junit (embutido)Coverage e teste são fluxos separados.

Watch local, isolamento por padrão

Na máquina local, o padrão é o oposto do CI, e é o que você quer. A flag --watch vem como true num TTY, então um ng test pelado na sua mesa re-roda ao salvar. Não há uma invocação --watch separada pra lembrar; o contexto do terminal decide.

Dois padrões do Vitest vale a pena conhecer, porque explicam tanto a velocidade quanto o flake ocasional. O pool padrão é forks, e os arquivos de teste rodam em paralelo entre processos worker, o que explica boa parte do motivo de a suíte parecer mais rápida que o Karma. Dentro de um mesmo arquivo, os testes ainda rodam em sequência. O isolamento é ligado por padrão: cada arquivo ganha um ambiente novo. Esse isolamento também é um botão. Desligar (--no-isolate, ou test.isolate: false numa config do Vitest) é mais rápido mas deixa estado vazar entre arquivos, então é uma alavanca de velocidade de CI que eu só puxaria com a suíte verde e uma medição dizendo que ajuda.

Artefato para reaproveitar

Checklist de prontidão de CI pra virada de coverage do Vitest

  • Adicione @vitest/coverage-v8 como devDependency e defina coverage: true em architect.test.options.
  • Fixe coverageReporters na config versionada: um reporter legível por humano (text-summary) mais um por máquina (lcov ou cobertura).
  • Rode ng test --coverage --no-watch uma vez e leia o número real do Vitest antes de definir qualquer threshold.
  • Defina coverageThresholds (statements/branches/functions/lines) no piso que a suíte passa hoje, não num número aspiracional.
  • Confirme que o build de fato falha abaixo do threshold (exit diferente de zero), não só imprime um aviso.
  • Adicione --reporters junit no comando de CI pra ter resultado por teste legível por máquina; mantenha separado dos reporters de coverage.
  • Passe --no-watch explícito no CI em vez de depender da detecção de process.env.CI.
  • Decida o provider de propósito: fique no v8 a não ser que precise da instrumentação por módulo do istanbul, e documente que o arquivo de runnerConfig não tem suporte do Angular.

Perguntas frequentes

Como habilito coverage com o builder Vitest do Angular?

Instale @vitest/coverage-v8 como devDependency e então passe ng test --coverage ou defina coverage: true em architect.test.options no angular.json. Coverage fica desligado por padrão até você pedir. Adicione coverageReporters (por exemplo lcov e text-summary) para controlar os formatos de saída.

Dá pra usar o provider istanbul no Angular em vez do v8?

Não por uma opção first-class do Angular. O CLI documenta só o @vitest/coverage-v8, e não há opção coverageProvider no builder. Trocar pro istanbul significa definir coverage.provider: 'istanbul' num vitest.config.ts alcançado pela opção runnerConfig, e o Angular explicitamente não dá suporte ao conteúdo desse arquivo. Desde o Vitest 3.2 o remapeamento por AST do provider v8 produz relatórios idênticos aos do istanbul, então a maioria dos times não tem motivo pra trocar.

Como faço os testes Angular falharem quando o coverage está baixo?

Defina coverageThresholds no angular.json com porcentagens de statements, branches, functions e lines. A doc do Angular diz que, se o coverage cair abaixo do piso configurado quando você roda os testes, o comando falha com exit diferente de zero. Defina o piso no que a suíte passa hoje e suba aos poucos, em vez de começar acima de onde você está.

Como rodo os testes Vitest do Angular uma vez só no CI, sem watch mode?

O Vitest autodetecta CI: ele entra em run mode quando process.env.CI está setado, e cai pra run mode quando o stdin não é um TTY. Na máquina local, o watch liga por padrão num terminal. Pro comando de CI eu ainda passo --no-watch explícito pra o comportamento não depender da detecção de ambiente. Adicione --reporters junit pra ter resultado por teste legível por máquina.

Qual a diferença entre coverageReporters e reporters no builder de teste do Angular?

reporters configura a saída de execução dos testes (nomes embutidos incluem default, verbose, junit, tap); coverageReporters configura a saída de coverage (text, lcov, lcovonly, cobertura, html, json, json-summary). São fluxos separados: o junit reporta quais testes passaram, enquanto o lcov ou o cobertura reporta quanto código eles cobriram.

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 · 7 min

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

O que o test target de Vitest do Angular v21 realmente é (uma linha), de onde vêm polyfills e styles, o install que tira o Karma, e as poucas opções que valem adicionar.

Ler artigo →

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 →