Modern Angular
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.
# 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ão | v8 (padrão do Angular) | istanbul (só via runnerConfig) |
|---|---|---|
| Pacote | @vitest/coverage-v8 | @vitest/coverage-istanbul |
| Como coleta | Runtime via node:inspector, source roda como está | Instrumenta os arquivos antes de rodar |
| Definido em | Só coverage: true no angular.json | coverage.provider: 'istanbul' numa config do Vitest |
| Suporte do Angular | First-class, documentado | Conteúdo sem suporte (runnerConfig) |
| Restringir a módulos | Não | Sim |
| Precisão vs istanbul | Idê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.
# 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-watchO 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 Karma | Equivalente no builder Vitest | Observação |
|---|---|---|
karma-coverage no karma.conf.js | coverage + coverageReporters no angular.json | Sem arquivo de config de coverage no caso comum. |
| Reporter istanbul, fixo | Provider v8; istanbul só via runnerConfig | Provider não é opção first-class do Angular. |
Thresholds no check do karma.conf.js | coverageThresholds (statements/branches/functions/lines) | Derruba a rodada abaixo do piso, mesmo portão. |
--no-watch --no-progress pro CI | Run mode autodetectado; passe --no-watch assim mesmo | Sem 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-v8como devDependency e definacoverage: trueemarchitect.test.options. - Fixe
coverageReportersna config versionada: um reporter legível por humano (text-summary) mais um por máquina (lcovoucobertura). - Rode
ng test --coverage --no-watchuma 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 junitno comando de CI pra ter resultado por teste legível por máquina; mantenha separado dos reporters de coverage. - Passe
--no-watchexplícito no CI em vez de depender da detecção deprocess.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
runnerConfignã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
- https://angular.dev/guide/testing/code-coverage
- https://angular.dev/cli/test
- https://angular.dev/guide/testing/migrating-to-vitest
- https://angular.dev/guide/testing
- https://vitest.dev/guide/coverage
- https://vitest.dev/config/coverage
- https://vitest.dev/guide/parallelism
- https://blog.angular.dev/announcing-angular-v21-57946c34f14b
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.
André Ramosdisponível para vagas remotas, UTC−3Entre em contato →