APRENDIZADO & COGNIÇÃO
92% de cobertura numa base herdada: contei as asserções antes de acreditar no número
Recebi acesso de leitura a um monolito em Node com TypeScript, algo perto de 200 mil linhas, seis anos de…

Recebi acesso de leitura a um monolito em Node com TypeScript, algo perto de 200 mil linhas, seis anos de histórico e quatro fornecedores diferentes no caminho. A primeira informação que me passaram sobre qualidade foi uma linha: “a cobertura está em 92%”.
Cobertura mede linha executada durante a suíte. Ela não mede verificação. Um teste que chama a função, não explode e termina conta a linha como coberta exatamente igual a um teste que compara o resultado com o esperado. Antes de abrir qualquer arquivo de produção, rodei um contador de asserções.
Primeira passada: por arquivo, com regex
Comecei pelo mais burro possível, porque roda em qualquer repositório sem instalar nada.
// conta-arquivos.jsconst { readFileSync } = require('node:fs');const { execSync } = require('node:child_process');const files = execSync( "find . -path ./node_modules -prune -o ( -name '*.spec.ts' -o -name '*.test.ts' ) -print", { encoding: 'utf8' }).trim().split('n').filter(Boolean);const ASSERCAO = /b(expect|assert|should|toMatchSnapshot)s*(/;const CASO = /b(it|test)s*(/g;let casos = 0;const mudos = [];for (const file of files) { const src = readFileSync(file, 'utf8'); casos += (src.match(CASO) || []).length; if (!ASSERCAO.test(src)) mudos.push(file);}console.log(`arquivos de teste: ${files.length}`);console.log(`casos (it/test): ${casos}`);console.log(`arquivos sem nenhuma asserção: ${mudos.length}`);mudos.slice(0, 5).forEach((f) => console.log(' ' + f));
Saída:
$ node conta-arquivos.jsarquivos de teste: 214casos (it/test): 1318arquivos sem nenhuma asserção: 37 src/billing/invoice.reprocess.spec.ts src/integrations/erp.sync.spec.ts src/reports/closing.spec.ts src/queue/retry-policy.spec.ts src/auth/session.cleanup.spec.ts
37 arquivos subiam módulo, chamavam função, esperavam promessa e não verificavam nada. Eles passam sempre. E cada linha que eles tocam entra na conta dos 92%.
Segunda passada: por caso, com AST
O número por arquivo é otimista, porque um arquivo com uma asserção no primeiro caso já sai da lista. O que importa é o caso, e aí a regex empaca. Troquei por ts-morph.
// conta-casos.jsconst { Project, SyntaxKind } = require('ts-morph');const project = new Project({ tsConfigFilePath: './tsconfig.json' });const alvos = new Set(['it', 'test', 'it.only', 'test.only']);let total = 0;const mudos = [];for (const sf of project.getSourceFiles(['**/*.spec.ts', '**/*.test.ts'])) { for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) { if (!alvos.has(call.getExpression().getText())) continue; total++; const corpo = call.getArguments()[1]; if (!corpo) continue; const verifica = corpo .getDescendantsOfKind(SyntaxKind.CallExpression) .some((c) => /^(expect|assert|chai.expect)/.test(c.getExpression().getText())); if (!verifica) { mudos.push(`${sf.getBaseName()} :: ${call.getArguments()[0].getText()}`); } }}const pct = ((mudos.length / total) * 100).toFixed(1);console.log(`casos: ${total}`);console.log(`casos sem asserção: ${mudos.length} (${pct}%)`);mudos.slice(0, 5).forEach((m) => console.log(' ' + m));
$ node conta-casos.jscasos: 1318casos sem asserção: 241 (18.3%) invoice.reprocess.spec.ts :: 'reprocessa fatura vencida' erp.sync.spec.ts :: 'sincroniza pedidos do dia' closing.spec.ts :: 'gera fechamento mensal' retry-policy.spec.ts :: 'respeita o backoff' session.cleanup.spec.ts :: 'limpa sessões expiradas'
Quase um em cada cinco casos não faz pergunta nenhuma ao sistema. Repare nos nomes: são justamente os fluxos que mexem em dinheiro e em integração externa. Ninguém escreve teste mudo para o formatador de CPF, porque ali a asserção é óbvia. O teste mudo nasce onde montar o cenário já deu trabalho demais e a pessoa parou quando o vermelho sumiu.
Um exemplar típico:
it('reprocessa fatura vencida', async () => { const fatura = await criarFatura({ status: 'vencida' }); await service.reprocessar(fatura.id);});
Esse caso percorre o serviço de faturamento inteiro, marca todas as linhas como cobertas e só falha se alguém jogar uma exceção não tratada. Troque a regra de juros por outra e ele continua verde.
O primo disso no monitoramento
O mesmo padrão aparece em observabilidade, e nessa base ele estava lá:
@Get('/health')health() { return { status: 'ok', uptime: process.uptime() };}
O endpoint responde 200 porque foi programado para responder 200. O painel fica verde com o banco fora do ar. A métrica existe, a garantia não. Substituí por uma versão que toca a dependência com timeout curto e devolve 503 quando ela não responde, que é uma das poucas mudanças que eu aceito fazer antes de entender o sistema.
O que eu faço com esse inventário
Nada de refatorar, e vou explicar por quê. Os 241 casos mudos são, ao mesmo tempo, o mapa das áreas que ninguém consegue testar direito e a lista do que está sem rede de proteção. Mexer neles antes de medir é trocar de lugar um problema que você ainda não conhece.
O que entra na primeira rodada é aditivo:
- Congelar os arquivos de produção que aparecem nos casos mudos e são também os mais alterados no último ano. É onde a regra de negócio vive.
- Instrumentar em vez de mexer: log estruturado nas rotas que esses fluxos atendem, captura de erro ligada, alarme no job noturno que falha calado.
- Rodar mutation testing num diretório só, não na suíte inteira. Em
src/billingo Stryker levou perto de 40 minutos e devolveu um score que não tinha relação nenhuma com os 92%. Na suíte completa levaria a madrugada e ninguém esperaria. - Escrever asserção primeiro nos casos mudos que já existem, aproveitando o cenário montado. É o teste mais barato de escrever que existe, porque o trabalho chato já está feito.
O teste do mutante é o único que responde à pergunta que interessa: se eu mudar o sinal daquela comparação, alguma coisa fica vermelha? Cobertura e contagem de asserção são degraus antes dele, e servem porque rodam em minutos.
Onde isso não serve
Não serve se o sistema está caindo agora. Com o checkout fora do ar, você estanca primeiro e mede depois, aceitando alguns dias às cegas. Também rende pouco em base pequena, coisa de 8 ou 10 mil linhas, onde ler os testes inteiros leva menos tempo que montar o instrumento.
E o script tem limite honesto: ele não enxerga asserção dentro de helper próprio. Se o time escreveu verificaFatura(resultado) com os expects lá dentro, o caso entra na lista de mudos sem ser mudo. Nessa base eu conferi na mão os 37 arquivos do primeiro passo e três eram falso positivo.
Rodei nos dois últimos sistemas que herdei e a faixa ficou entre 12% e 20% de casos sem asserção. Queria saber se isso é o normal do mercado ou azar meu. Se você rodar aí, comenta o número: quantos por cento dos seus casos não verificam nada, e o score de mutação bateu com a cobertura quando você foi conferir?
· · ·
Publicado originalmente no blog da Revin: https://revin.com.br/pt/blog/codigo-espaguete-herdado-7-primeiros-dias
Perguntas frequentes
Respostas institucionais sobre esta cobertura, fontes e limites editoriais.
- Sobre o que trata “92% de cobertura numa base herdada: contei as asserções antes…”?
- Recebi acesso de leitura a um monolito em Node com TypeScript, algo perto de 200 mil linhas, seis anos de histórico e quatro fornecedores diferentes no caminho. A primeira informação que me passaram sobre qualidade foi…
- O que a matéria explica sobre “Primeira passada: por arquivo, com regex”?
- Comecei pelo mais burro possível, porque roda em qualquer repositório sem instalar nada.// conta-arquivos.jsconst { readFileSync } = require('node:fs');const { execSync } = require('node:child_process');const files = execSync( "find . -path ./node_modules -prune -o ( -name '*.spec.ts' -o -name '*.test.ts' ) -print", {…
- O que a matéria explica sobre “Segunda passada: por caso, com AST”?
- O número por arquivo é otimista, porque um arquivo com uma asserção no primeiro caso já sai da lista. O que importa é o caso, e aí a regex empaca. Troquei por ts-morph.// conta-casos.jsconst { Project, SyntaxKind } = require('ts-morph');const project =…
- O que a matéria explica sobre “O primo disso no monitoramento”?
- O mesmo padrão aparece em observabilidade, e nessa base ele estava lá:@Get('/health')health() { return { status: 'ok', uptime: process.uptime() };}O endpoint responde 200 porque foi programado para responder 200. O painel fica verde com o banco fora do ar. A métrica existe,…
- O que a matéria explica sobre “O que eu faço com esse inventário”?
- Nada de refatorar, e vou explicar por quê. Os 241 casos mudos são, ao mesmo tempo, o mapa das áreas que ninguém consegue testar direito e a lista do que está sem rede de proteção. Mexer neles antes de medir é trocar…
- Quem edita o conteúdo do Estrato Mente?
- A cobertura é produzida e revisada pela equipe editorial do Estrato Mente, com editor-chefe responsável pela linha editorial. Conheça a redação em https://mente.estrato.cc/equipe/.
- Quais fontes o Estrato prioriza?
- Priorizamos fontes primárias (órgãos oficiais, balanços, estudos revisados, documentos públicos) e cruzamos informações antes da publicação, conforme a política editorial.
- Como solicitar correção de uma matéria?
- Envie o pedido pela página de Correções (https://mente.estrato.cc/correcoes/) ou Contato (https://mente.estrato.cc/contato/). Erros materiais são corrigidos com registro da atualização.