Cobertura no ecossistema JS

TL;DR

vitest run --coverage mede quanto do código os testes exercitaram, em quatro dimensões: linhas, statements, funções e branches (a de branches é a que mais importa). O Vitest oferece dois provedores: v8 (usa a cobertura nativa do motor V8 — rápido, o default) e istanbul (instrumenta o código — mais preciso em casos de borda). Você configura thresholds que falham o build abaixo de um percentual. A lição central (a mesma de Testes 12): cobertura mede o que foi executado, não o que foi verificado — 100% de cobertura com asserções fracas não prova nada. É um detector de buracos, não um selo de qualidade.

O problema: “quanto do meu código está testado?”

É uma pergunta legítima — e perigosa. Legítima porque você quer saber se há partes do código que nenhum teste toca (candidatas óbvias a bugs). Perigosa porque a resposta, um número percentual, é fácil de transformar numa meta idólatra (“temos que chegar a 100%”) que produz testes inúteis escritos só para subir o número.

A cobertura é uma ferramenta valiosa quando você entende o que ela mede e o que não mede. Esta nota é o ferramental JS (como rodar, os provedores, os thresholds) ancorado na lição conceitual que a Engenharia/Testes já estabeleceu: cobertura alta ≠ testes bons.

Rodar e ler a cobertura

vitest run --coverage

(Requer instalar o provider: npm i -D @vitest/coverage-v8.) A saída é uma tabela por arquivo com quatro métricas:

MétricaMedeNota
% Stmtsstatements executadosgranular
% Branchramos (if/else, &&, ternários, ?.) percorridosa mais reveladora
% Funcsfunções chamadaspega funções mortas
% Lineslinhas executadasa mais citada, a menos informativa

A métrica que mais importa é branches: 100% de linhas com 50% de branches significa que você roda o código mas nunca testa o caminho do else, do erro, do caso vazio — exatamente onde os bugs moram. ”% Lines” alto com ”% Branch” baixo é o retrato de uma suíte que passa por tudo sem testar as decisões.

Os dois provedores: v8 vs istanbul


graph TB
    A["--coverage"] --> B{provider}
    B --> C["v8 (default)<br/>cobertura nativa do motor<br/>rápido, sem instrumentar"]
    B --> D["istanbul<br/>instrumenta o código<br/>mais preciso, mais lento"]
    style C fill:#4A90D9,color:#fff
    style D fill:#4A90D9,color:#fff
  • v8 (@vitest/coverage-v8): usa a cobertura que o motor V8 já coleta nativamente. Não instrumenta seu código, então é rápido e não distorce o que roda. É o default e a escolha da maioria. Historicamente tinha imprecisões em mapear de volta ao código-fonte (via source maps), muito melhoradas ao longo do tempo.
  • istanbul (@vitest/coverage-istanbul): instrumenta o código (insere contadores) antes de rodar. É mais preciso em casos de borda de mapeamento, ao custo de ser mais lento. Escolha-o se você notar números de cobertura estranhos com o v8.

Config no vitest.config:

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',                    // ou 'istanbul'
      reporter: ['text', 'html', 'lcov'], // terminal + relatório navegável + p/ CI
      exclude: ['**/*.config.*', 'src/types/**'],
      thresholds: {
        lines: 80, functions: 80, branches: 75, statements: 80,
      },
    },
  },
});

Os thresholds fazem o --coverage falhar se a cobertura cair abaixo do percentual — um gate de CI contra regressão de cobertura. O reporter lcov gera o formato que serviços de CI (Codecov, etc.) consomem; o html gera um relatório navegável onde você vê linha a linha o que não foi coberto.

A armadilha: cobertura não é qualidade

Perseguir 100% de cobertura como meta

O que acontece: o time impõe “100% de cobertura”, e surgem testes que executam o código sem verificar nada — chamam a função, não afirmam o resultado, só para a linha “contar como coberta”. Por quê: cobertura mede execução, não verificação. Um teste expect(fn()).toBeDefined() cobre a função inteira e não testa quase nada. 100% de cobertura com asserções vazias é 0% de confiança — e a meta de 100% incentiva exatamente esses testes-fantasma, além de custar caro nos últimos e menos valiosos pontos. Como evitar: trate cobertura como detector de buracos (o que nenhum teste toca), não como selo. Mire numa faixa pragmática (70–85% costuma ser saudável), priorize branches sobre linhas, e para medir se os testes realmente pegam bugs use mutation testing (ver Testes 12; a ferramenta JS é o Stryker).

Cobertura no ecossistema JS em uma frase: vitest --coverage mede linhas/statements/funcs/branches (priorize branches) via provider v8 (rápido, default) ou istanbul (preciso), com thresholds que barram regressão — lembrando que cobertura mede execução, não verificação, então é um detector de buracos, não um selo de qualidade.

Em entrevista

vitest run --coverage reports lines, statements, functions, and branches — and branches is the one I watch, because high line coverage with low branch coverage means I’m running the code but never testing the else, the error path, the empty case. Vitest has two providers: v8, which uses the engine’s native coverage and is fast — the default — and istanbul, which instruments the code and is more precise. I set thresholds to fail the build on regressions. But the key point is that coverage measures execution, not verification: 100% coverage with weak assertions proves nothing. It’s a hole detector, not a quality badge — for real bug-catching power I’d add mutation testing with Stryker.”

PTEN
Cobertura de testesTest coverage
Ramo (branch)Branch
Instrumentar o códigoInstrument the code
Limiar (threshold)Threshold
Detector de buracosHole detector
Teste de mutaçãoMutation testing

O que vem a seguir

Fechamos a fase Adepto — o ferramental de unit, componente, rede, hooks e qualidade. A fase Magus sobe a pirâmide até o topo: os testes end-to-end no browser real, começando pela ferramenta que dominou a categoria, o Playwright.

Fontes

  • VitestCoverage--coverage, v8 vs istanbul, thresholds, reporters.
  • Istanbulistanbul.js.org — o instrumentador por trás do provider istanbul.
  • Strykerstryker-mutator.io — mutation testing em JS, a resposta ao “cobertura não é qualidade”.