680 Execuções, Zero Retentativas: Como Realmente Provar que um Teste Flaky Foi Corrigido


“O teste flaky foi corrigido — ele passou.”
Não. Ele passou uma vez. Isso não é correção.
Se a taxa de falha original era 0,5%, uma única execução bem-sucedida te dá 99,5% de confiança. Parece alto até o seu CI rodar 200 vezes por semana e você ver falhas “aleatórias” toda segunda-feira de manhã. Uma execução bem-sucedida não é evidência. É uma moeda jogada para o alto que você teve sorte de acertar.
Eu executo cada correção de teste flaky 680 vezes consecutivas antes de considerar resolvido. Zero retentativas. Zero allowedFlakes. Se falhar uma vez em 680 execuções, a correção não é real.
A Matemática Que Muda Sua Perspectiva
Esse é um problema de probabilidade binomial. Se um teste tem uma taxa real de falha p, a probabilidade de ele passar n execuções consecutivas é (1 − p)^n. A probabilidade de ver pelo menos uma falha é 1 − (1 − p)^n.
Para um teste com taxa de falha de 0,5%:
| Passes Consecutivos | P(todos passam) | P(pelo menos 1 falha) | Confiança de que o bug ainda existe |
|---|---|---|---|
| 1 | 99,50% | 0,50% | Quase nenhuma — você não aprendeu nada |
| 10 | 95,11% | 4,89% | Baixa |
| 50 | 77,83% | 22,17% | Moderada |
| 100 | 60,65% | 39,35% | Começando a chegar em algum lugar |
| 200 | 36,77% | 63,23% | Cara ou coroa |
| 460 | 10,00% | 90,00% | 90% de confiança de que a correção é real |
| 680 | 3,31% | 96,69% | ~97% de confiança com taxa de falha de 0,5% |
| 1000 | 0,67% | 99,33% | 99%+ de confiança |
Existe também um atalho útil chamado Regra de Três: se você observa zero falhas em n tentativas, o limite superior da taxa de falha com 95% de confiança é aproximadamente 3/n. Para n = 680, isso é 3/680 = 0,44%. Então, 680 passes com zero falhas significa que você pode ter 95% de confiança de que a taxa real de falha está abaixo de 0,44%.
Por que 680 especificamente? É um número prático: 8 workers paralelos x 85 iterações cada. Cabe em uma única execução de stress, completa em tempo razoável e empurra o limite superior das taxas de falha prováveis para abaixo do limiar em que um pipeline de CI típico veria falhas semanais.
O ponto não é que 680 seja um número mágico. O ponto é que um não é suficiente, dez não é suficiente, e você precisa fazer as contas para a sua situação específica.
Por Que a Maioria das “Correções” Falha
Após meses executando esse protocolo, três padrões emergiram nas correções que falharam — testes que passaram uma vez ou até cinquenta vezes e depois falharam novamente.
Padrão 1: O Timeout Estendido
A não-correção mais comum. Um teste falha porque um elemento leva 6 segundos para aparecer. A “correção” muda o timeout de 5 segundos para 15 segundos. Passa hoje. Amanhã, sob carga de CI com workers de teste paralelos competindo por recursos, aquele elemento leva 18 segundos.
// ❌ The non-fix: extending the timeout
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Success')).toBeVisible({ timeout: 15000 });
// ✅ The real fix: wait for the actual condition, not an arbitrary timer
await page.getByRole('button', { name: 'Submit' }).click();
await page.waitForResponse(resp =>
resp.url().includes('/api/submit') && resp.status() === 200
);
await expect(page.getByText('Success')).toBeVisible();Um timeout não é uma correção — é uma aposta de que o sistema sempre será pelo menos tão rápido. Essa aposta perde eventualmente.
Padrão 2: O Vazamento de Estado
O Teste B passa quando executado sozinho, falha quando executado após o Teste A. O Teste A deixa para trás um cookie, uma entrada no local storage ou uma linha no banco de dados que muda as condições iniciais do Teste B. A “correção” adiciona um cleanup no beforeEach. Mas o cleanup em si é frágil — ele limpa o estado conhecido mas perde o artefato que só é criado sob condições específicas do Teste A.
// ❌ Fragile cleanup: clearing known state
test.beforeEach(async ({ page }) => {
await page.evaluate(() => localStorage.clear());
});
// ✅ Robust isolation: fresh context per test
// In playwright.config.ts — each test gets a pristine browser context
const config: PlaywrightTestConfig = {
use: {
// Every test starts with zero cookies, zero storage, zero history
storageState: undefined,
contextOptions: {
ignoreHTTPSErrors: true,
},
},
// Fully parallel — no shared state between workers
fullyParallel: true,
};Padrão 3: A Condição de Corrida
Um clique dispara. Uma requisição de rede começa. O teste faz a asserção sobre o resultado. Mas a asserção executa antes da resposta chegar. Funciona na sua máquina local rápida. Falha 0,5% das vezes no CI, onde a contenção de recursos adiciona 200ms de latência a cada salto de rede.
// ❌ Race condition: assert before the data arrives
await page.getByRole('button', { name: 'Load Data' }).click();
await expect(page.getByTestId('results-count')).toHaveText('42');
// ✅ Wait for the network response, then assert on the rendered result
await page.getByRole('button', { name: 'Load Data' }).click();
await page.waitForResponse(resp =>
resp.url().includes('/api/data') && resp.status() === 200
);
await expect(page.getByTestId('results-count')).toHaveText('42');Pare de Dizer “Flaky” — Comece a Classificar
A palavra “flaky” é um beco sem saída diagnóstico. Ela interrompe a investigação. É o equivalente em testes de um médico dizer “você está doente.” Tecnicamente verdade. Completamente inútil.
Toda falha de teste tem uma causa raiz. Classifique-a:
| Classe da Causa Raiz | Sintoma | Categoria da Correção |
|---|---|---|
| Defeito de timing | Passa localmente, falha no CI sob carga | Espere por condições, não por timers |
| Vazamento de estado | Falha somente quando executado após testes específicos | Isolamento — contexto novo por teste |
| Condição de corrida | Falha intermitentemente em asserções rápidas | Espere pela rede/estado, depois faça a asserção |
| Contenção de recursos | Falha em paralelo, passa em serial | Isolamento de workers ou locks de recursos |
| Divergência de ambiente | Falha em staging mas não em dev | Fixtures com consciência de ambiente |
Uma vez que você nomeia a classe, você pode buscá-la. Isso é hardening preditivo.
Hardening Preditivo: Corrija Testes Antes que Eles Falhem
Essa é a mudança metodológica que importa mais do que o próprio protocolo de 680 execuções.
Depois de corrigir 3 testes flaky conhecidos que compartilhavam a mesma causa raiz — condições de corrida entre handlers de clique e respostas de rede — eu busquei em toda a suíte pelo mesmo padrão. Grep por declarações de asserção que seguem imediatamente ações de clique sem um waitForResponse ou waitForLoadState intermediário.
# Find potential race conditions: assertions immediately after clicks
# with no waitForResponse in between
grep -n "\.click()" tests/**/*.spec.ts | while read line; do
file=$(echo "$line" | cut -d: -f1)
linenum=$(echo "$line" | cut -d: -f2)
# Check if the next 3 lines contain a waitForResponse
nextlines=$(sed -n "$((linenum+1)),$((linenum+3))p" "$file")
if echo "$nextlines" | grep -q "expect\|toHave\|toBe" && \
! echo "$nextlines" | grep -q "waitFor"; then
echo "POTENTIAL RACE: $file:$linenum"
fi
doneEncontrei mais 3 testes com o mesmo padrão de vulnerabilidade — e os fortaleci antes que jamais falhassem no CI.
Essa é a diferença entre testes reativos (esperar quebrar, depois corrigir) e hardening preditivo (classificar o padrão de falha, depois varrer a suíte).
O Padrão FAST / STANDARD / EXTENDED
Substitua magic numbers ad-hoc por constantes de polling padronizadas. Cada chamada waitFor na suíte referencia uma constante nomeada, não um chute:
// test-constants.ts
export const TIMEOUTS = {
FAST: 2_000, // Elements that should appear immediately after navigation
STANDARD: 10_000, // API responses and re-renders
EXTENDED: 30_000, // Complex operations, streaming, file uploads
STRESS: 60_000, // Only used in stress test configurations
} as const;
// In tests:
await expect(page.getByRole('heading')).toBeVisible({
timeout: TIMEOUTS.FAST
});
await expect(page.getByTestId('search-results')).toHaveCount(10, {
timeout: TIMEOUTS.STANDARD
});Quando um teste precisa de EXTENDED, isso é um sinal. Significa que ou a operação é genuinamente lenta (aceitável) ou algo na arquitetura está bloqueando (investigue). Constantes nomeadas tornam esses sinais visíveis no code review.
O Protocolo de Teste de Stress
Aqui está a configuração exata que eu uso para validação de stress:
// playwright.stress.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
workers: 8,
repeatEach: 85, // 8 × 85 = 680 total runs
retries: 0, // Zero tolerance — a single failure invalidates the fix
timeout: 30_000,
use: {
trace: 'on-first-retry', // Won't fire with 0 retries — that's the point
},
reporter: [
['list'],
['json', { outputFile: 'stress-results.json' }],
],
});# Run stress validation against a specific test file
npx playwright test tests/checkout-flow.spec.ts --config=playwright.stress.config.ts
# Verify: all 680 must pass
cat stress-results.json | jq '.stats | {total: .expected, passed: .expected, failed: .unexpected}'Se todas as 680 passarem: a correção é real. Faça o deploy.
Se alguma falhar: não olhe apenas para a contagem de falhas. Olhe para quais workers falharam e quais iterações dentro desses workers. Falhas concentradas em iterações posteriores sugerem esgotamento de recursos (vazamento de memória, esgotamento do pool de conexões). Falhas espalhadas aleatoriamente sugerem que a correção está incompleta. Falhas apenas em workers específicos sugerem um problema de isolamento.
A Lição Mais Profunda
Testes flaky não são uma categoria de incômodo. Eles são uma oportunidade diagnóstica. Todo teste “flaky” está te dizendo algo específico sobre o comportamento do seu sistema sob condições para as quais você não projetou. O teste que falha intermitentemente no CI mas passa localmente está reportando informação real: sua aplicação se comporta de maneira diferente sob contenção de recursos. Não é o teste sendo não confiável. É o teste sendo mais honesto do que o seu ambiente local.
A mudança de “corrigir esse teste flaky” para “classificar e eliminar esse padrão de falha em toda a suíte” é a mudança mais impactante que eu fiz na minha metodologia de testes. Ela transforma um jogo reativo de whack-a-mole em uma redução sistemática da superfície de falha.
Pare de chamar seus testes de flaky. Comece a classificar suas falhas. E pare de chamar uma correção de pronta porque ela passou uma vez.
680 execuções. Zero retentativas. Confiança estatística, não esperança.
