Deletamos 37 testes e nossa cobertura melhorou

Erik Treviño avatar
Erik Treviño
Cover for 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ótuloQuantidadePorcentagemAção
WORKFLOW13474%Manter — esses são a suite
FLUFF3721%Remover — cobertos por outras camadas ou por testes workflow
MERGE21%Combinar em 1 teste cada
KEEP_DETAILED74%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.ts

Rodada 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étricaAntes (180 testes)Depois (141 testes)Mudança
Duração total da suite24 min 12 seg17 min 45 seg-27%
Taxa de testes flaky3,2% das execuções1,1% das execuções-66%
Cobertura workflow única134 caminhos134 caminhosSem 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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ã

  1. Exporte sua lista de arquivos de teste. Cada arquivo de teste E2E da sua suite, um por linha.

  2. 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.”

  3. 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?

  4. Aplique os quatro rótulos. WORKFLOW, FLUFF, MERGE, KEEP_DETAILED.

  5. 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.

  6. 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.