Deletamos 37 testes e nossa cobertura melhorou


Deletamos 37 testes no mês passado.
Nossa cobertura melhorou.
Não “ficou igual” — melhorou. A suite rodou mais rápido, teve menos flakes, e os testes WORKFLOW que restaram já cobriam cada caminho que os testes deletados verificavam. Não perdemos nenhuma asserção significativa. Perdemos peso morto.
O que a maioria dos times não vai admitir: uma suite de testes que só cresce é como um codebase que só cresce. Eventualmente o custo de manutenção supera o valor. Todo time tem o instinto de “vamos adicionar mais testes”. Quase nenhum tem a disciplina de “vamos auditar o que já temos”.
O sistema de classificação de quatro rótulos
Eu construí um classificador de testes. Não uma ferramenta de IA — uma taxonomia. Uma forma sistemática de olhar para cada teste em uma suite e responder uma pergunta: Esse teste merece seu lugar?
Cada teste recebe um dos quatro rótulos:
- WORKFLOW — Testa uma jornada real do usuário de ponta a ponta. Login → navegar → executar ação → verificar resultado. Esses são a espinha dorsal. Ficam.
- FLUFF — Verifica algo que já está coberto por outro teste em uma camada melhor. O teste de validação do formulário de login que também existe como teste unitário. Remove.
- MERGE — Dois testes que compartilham 80% do setup e das asserções mas testam branches levemente diferentes. Combina em um único teste parametrizado.
- KEEP_DETAILED — Um caso extremo que genuinamente importa (tratamento de fuso horário, limites de permissão, caminhos de migração de dados). Fica, mas avalia se pertence à camada E2E ou se poderia migrar para integração/unitário.
A taxonomia é deliberadamente simples. Quatro rótulos. Sem escala de ambiguidade. Sem categoria “talvez”. Cada teste recebe uma classificação definitiva.
Como cada rótulo se manifesta na prática
WORKFLOW: uma jornada real do usuário
// WORKFLOW — This test earns its place. It validates a complete user journey
// that can only be tested through a browser.
test('user creates a report and shares it with a teammate', async ({ page }) => {
await page.goto('/reports');
await page.getByRole('button', { name: 'New Report' }).click();
// Fill out the report form
await page.getByLabel('Report Name').fill('Q1 Sales Summary');
await page.getByRole('combobox', { name: 'Template' }).selectOption('quarterly');
await page.getByRole('button', { name: 'Generate' }).click();
// Wait for the report to generate (involves backend processing)
await page.waitForResponse(resp =>
resp.url().includes('/api/reports') && resp.status() === 201
);
await expect(page.getByText('Q1 Sales Summary')).toBeVisible();
// Share with a teammate
await page.getByRole('button', { name: 'Share' }).click();
await page.getByLabel('Email').fill('teammate@company.com');
await page.getByRole('button', { name: 'Send' }).click();
await expect(page.getByText('Report shared successfully')).toBeVisible();
});Esse teste exercita navegação, envio de formulário, processamento backend, renderização e uma ação secundária. Só pode rodar no navegador. Valida uma história de usuário real. WORKFLOW.
FLUFF: já coberto em outro lugar
// FLUFF — This test validates form validation rules through the browser.
// The exact same validation logic is covered by unit tests on the
// validation schema. The E2E test adds 45 seconds of browser execution
// to verify something the unit test covers in 12 milliseconds.
test('report name field shows error when empty', async ({ page }) => {
await page.goto('/reports');
await page.getByRole('button', { name: 'New Report' }).click();
await page.getByLabel('Report Name').fill('');
await page.getByLabel('Report Name').blur();
await expect(page.getByText('Report name is required')).toBeVisible();
});Esse teste não está errado. A asserção é válida. Mas o teste WORKFLOW acima já navega até esse formulário. Se a validação estivesse quebrada, o teste workflow falharia na etapa de envio do formulário. O teste FLUFF adiciona 45 segundos de tempo de execução para re-verificar uma regra de validação que um teste unitário cobre em milissegundos.
MERGE: dois testes que deveriam ser um só
// MERGE candidate A
test('admin can delete a report', async ({ page }) => {
await loginAs(page, 'admin');
await page.goto('/reports');
await page.getByRole('row', { name: 'Q1 Sales' }).getByRole('button', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByRole('row', { name: 'Q1 Sales' })).not.toBeVisible();
});
// MERGE candidate B — 90% identical setup, just checks a different element
test('admin sees delete confirmation dialog', async ({ page }) => {
await loginAs(page, 'admin');
await page.goto('/reports');
await page.getByRole('row', { name: 'Q1 Sales' }).getByRole('button', { name: 'Delete' }).click();
await expect(page.getByRole('dialog', { name: 'Confirm deletion' })).toBeVisible();
});Esses dois testes compartilham o mesmo setup, a mesma navegação, o mesmo papel e a mesma ação inicial. O teste B é um subconjunto do teste A. Fazemos o merge — o primeiro teste já clica no diálogo de confirmação e verifica a exclusão.
Os resultados da auditoria
Apliquei essa taxonomia a uma suite E2E de 180 testes. Não em uma única passada — três rodadas de classificação com revisão manual em cada etapa.
| Rótulo | Quantidade | Porcentagem | Ação |
|---|---|---|---|
| WORKFLOW | 134 | 74% | Manter — esses são a suite |
| FLUFF | 37 | 21% | Remover — cobertos por outras camadas ou por testes workflow |
| MERGE | 2 | 1% | Combinar em 1 teste cada |
| KEEP_DETAILED | 7 | 4% | Manter no E2E, ou migrar para a camada de integração |
21% da suite era FLUFF. Um em cada cinco testes consumia tempo de CI, contribuía para as taxas de flake e exigia manutenção — sem fornecer absolutamente nenhuma cobertura única.
O ciclo de auditoria em três rodadas
Uma classificação de passada única não é confiável. Eu perdi classificações na primeira rodada, peguei na segunda e refinei as regras da taxonomia na terceira.
Rodada 1: classificação inicial. Aplicar os quatro rótulos a cada arquivo de teste. Usar análise estática para identificar testes que não contêm ações de navegação (provavelmente FLUFF), testes que compartilham mais de 80% dos seus locators com outro teste (provavelmente MERGE) e testes que verificam apenas um único elemento (provavelmente FLUFF ou KEEP_DETAILED).
# Quick static analysis: find tests that never call page.goto() or navigate
# These are often fluff tests that rely on another test's setup
grep -rL "page\.goto\|page\.click.*nav\|page\.getByRole.*link" tests/e2e/*.spec.tsRodada 2: revisão manual de cada rótulo FLUFF. Para cada teste rotulado como FLUFF, responda duas perguntas: (1) Existe um teste WORKFLOW que já cobre esse caminho? (2) Um teste unitário ou de integração já verifica esse mesmo comportamento? Se ambas as respostas forem sim, o rótulo FLUFF se mantém. Se alguma for não, reclassifique.
Rodada 3: refinar e re-auditar. Atualize as regras de classificação com base no que você aprendeu na rodada 2. Execute novamente a análise estática. Verifique falsos negativos — testes que você rotulou como WORKFLOW que na verdade deveriam ser MERGE ou KEEP_DETAILED.
Três rodadas. Não uma. O custo de um falso positivo (deletar um teste que fornece cobertura única) é muito maior que o custo de uma rodada extra de auditoria.
Antes e depois
Performance da suite
| Métrica | Antes (180 testes) | Depois (141 testes) | Mudança |
|---|---|---|---|
| Duração total da suite | 24 min 12 seg | 17 min 45 seg | -27% |
| Taxa de testes flaky | 3,2% das execuções | 1,1% das execuções | -66% |
| Cobertura workflow única | 134 caminhos | 134 caminhos | Sem mudança |
| Horas semanais de manutenção | ~4 hrs | ~2,5 hrs | -38% |
A queda na taxa de flake é a métrica mais reveladora. Os 37 testes removidos tinham a maior taxa de flake da suite — porque estavam testando detalhes de implementação pela camada mais frágil possível. Uma regra de validação de formulário testada pelo navegador é sensível ao timing de renderização, aos mutation observers do DOM e ao gerenciamento de foco. A mesma regra testada em um teste unitário tem zero superfície de flake.
O que não perdemos
Este é o número que importa: 134 caminhos workflow antes, 134 caminhos workflow depois. Cada jornada real de usuário que era testada antes da auditoria continua sendo testada depois. Não perdemos cobertura — perdemos redundância.
A distinção importa. Cobertura que existe em duas camadas (unitário + E2E) não é “mais cobertura” do que cobertura que existe em uma única camada correta. É custo duplicado com a mesma proteção.
Por que os testes FLUFF se acumulam
Testes FLUFF não são criados por engenheiros descuidados. Eles se acumulam por razões estruturais:
Camadas de teste faltando. Quando seu codebase não tem uma camada de teste de componentes nem uma camada de teste de integração de API, o E2E vira o depósito de tudo. Cada verificação de comportamento, cada caso extremo, cada “deixa eu só verificar essa coisinha” acaba no E2E porque não tem outro lugar para colocar.
A suposição de “mais testes = melhor”. Métricas de velocidade do time que contam testes adicionados por sprint incentivam quantidade sobre arquitetura. Ninguém recebe crédito por deletar um teste, mesmo quando a exclusão melhora a suite.
Criação de testes por copiar e colar. Um desenvolvedor copia um teste E2E existente como template, muda a asserção, e agora existem dois testes que compartilham 90% do setup. Nenhum está errado individualmente. Juntos, são candidatos a MERGE.
Medo de remover testes. “E se a gente precisar depois?” Isso é o equivalente em testes da síndrome de acumulador. Se a cobertura existe em outra camada, remover o teste E2E não remove a rede de segurança — remove a duplicata.
O protocolo de auditoria: o que você pode fazer segunda-feira de manhã
Exporte sua lista de arquivos de teste. Cada arquivo de teste E2E da sua suite, um por linha.
Rotule cada teste com sua asserção principal. O que esse teste está realmente verificando? “O usuário consegue fazer login.” “Mensagem de erro aparece com entrada inválida.” “Os dados carregam após a navegação.”
Cruze com seus testes unitários e de integração. Para cada asserção E2E, um teste mais rápido já cobre o mesmo comportamento?
Aplique os quatro rótulos. WORKFLOW, FLUFF, MERGE, KEEP_DETAILED.
Revise os rótulos FLUFF com o time. Não delete unilateralmente. Mostre ao time quais testes você está propondo remover e quais testes workflow já cobrem esses caminhos.
Delete em um único PR com métricas de antes e depois. Execute a suite completa antes e depois. Meça a duração, a taxa de flake e os caminhos únicos cobertos. Os números devem falar por si.
A arte da subtração
Todo engenheiro sabe como adicionar um teste. Escrever o setup, escrever a asserção, ver passar, commitar. Parece produtivo. A contagem de testes sobe. O relatório de cobertura mostra verde.
Remover um teste exige um conjunto de habilidades diferente. Você precisa entender o que o teste realmente cobre (não o que o nome diz), se essa cobertura existe em outro lugar da suite, se a asserção pertence a essa camada, e se removê-lo cria uma lacuna ou apenas elimina uma duplicata.
Isso é mais difícil. Exige entender a arquitetura completa de testes, não apenas o arquivo de teste individual. Exige confiança de que sua suite restante é suficiente — confiança respaldada por análise, não por esperança.
Uma suite enxuta, focada em workflows, com 141 testes que rodam em 17 minutos, é mais valiosa que uma suite inchada com 180 testes que roda em 24 minutos e tem o dobro de flakes. A suite menor é mais rápida de executar, mais barata de manter, mais confiável de interpretar, e cobre exatamente as mesmas jornadas de usuário.
Adicionar testes é fácil. Remover testes é engenharia.
