---
title: "Coverage e CI do Vitest no Angular: providers, thresholds e o gap do Karma"
description: "Como coverage e CI funcionam com o builder Vitest do Angular: a flag ng test --coverage, os coverageThresholds que derrubam o build, o reporter junit pro CI, e a única coisa que o builder não expõe e o karma-coverage expunha."
deck: "O coverage é [first-class no Angular CLI](https://angular.dev/guide/testing/code-coverage): 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](https://angular.dev/guide/testing/migrating-to-vitest)."
author: "André Ramos"
url: "https://andreramos.dev/pt/angular/vitest-coverage-and-ci-angular/"
lang: "pt-BR"
type: "article"
---

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

> O coverage é [first-class no Angular CLI](https://angular.dev/guide/testing/code-coverage): 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](https://angular.dev/guide/testing/migrating-to-vitest).

## 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 mostra*

```text
# 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.

*Checagem local de coverage antes de confiar no threshold*

```bash
# 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 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 reaproveitável: 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

- 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

## Leia também

- [De Karma para Vitest no Angular: a config que realmente roda](https://andreramos.dev/pt/angular/karma-to-vitest-angular-config/)
- [Quando migrar uma suíte Angular para Vitest](https://andreramos.dev/pt/angular/move-angular-test-suite-to-vitest/)
- [fakeAsync e o erro ProxyZone: consertando testes Angular no Vitest](https://andreramos.dev/pt/angular/angular-vitest-fakeasync-proxyzone-error/)
