Um teste que falha aleatoriamente em código não alterado ensina o time a reexecutar o CI em vez de investigar, e esse hábito é como falhas reais passam despercebidas. Testes flaky corroem a confiança em toda a suite.
Por que testes flaky são piores do que nenhum teste
Quando um teste falha de forma consistente, você o corrige. Quando falha aleatoriamente, o time começa a ignorar builds vermelhos. "Provavelmente é só o teste flaky de login" se torna a resposta padrão para falhas de CI. Eventualmente um bug real passa despercebido porque ninguém levou a sério o build vermelho.
Testes flaky corroem a confiança em toda a suite de testes. Por isso, corrigi-los vale o tempo, mesmo quando o próprio teste não é crítico.
As causas mais comuns
Antes de debugar, saiba o que está procurando. Testes flaky quase sempre vêm de um desses cinco lugares.
Problemas de timing. A causa mais comum de longe. O teste tenta interagir com um elemento antes de ele estar pronto: antes de aparecer, antes de estar habilitado, antes de uma animação terminar. O teste passa quando a página carrega rápido e falha quando carrega devagar. Poluição de teste. Um teste deixa para trás estado que quebra o próximo. Um registro criado, um cookie esquecido, um valor de localStorage modificado. Testes que passam sozinhos mas falham em uma suite são quase sempre isso. Dados de teste compartilhados. Dois testes rodam em paralelo e ambos tentam usar ou modificar o mesmo registro. Um ganha, o outro falha. Dependências de rede. Um teste faz uma chamada real de API que ocasionalmente torna o timeout ou retorna dados inesperados. Instabilidade na ordem de elementos. Um teste assume que os elementos aparecem em uma ordem específica (primeira linha, segundo botão) mas a ordem não é garantida.Comece com o Playwright trace viewer
Antes de alterar qualquer código, reproduza a falha e capture um trace. O trace viewer é a ferramenta de debug mais poderosa do Playwright: ele grava toda ação, requisição de rede e snapshot de DOM durante uma execução de teste.
Ative o tracing no playwright.config.ts:
export default defineConfig({
use: {
trace: 'on-first-retry', // captura trace quando um teste falha e faz retry
},
retries: 1, // faz retry uma vez para o trace ser capturado
});Execute os testes e abra o relatório:
npx playwright test
npx playwright show-reportClique em um teste que falhou. A visualização do trace mostra uma linha do tempo de toda ação com screenshots antes/depois. Você consegue ver exatamente qual passo falhou, como a página parecia naquele momento, e quais requisições de rede estavam em andamento.
Só isso resolve aproximadamente metade das investigações de testes flaky sem nenhum chute.
Corrigindo problemas de timing
Problemas de timing aparecem assim no output de erro:
Error: locator.click: Timeout 30000ms exceeded.
waiting for getByRole('button', { name: 'Submit' })Ou:
Error: expect(locator).toBeVisible()
Received: hiddenO instinto é adicionar um wait. A correção errada:
// Ruim — chutando quanto tempo esperar
await page.waitForTimeout(2000);
await page.getByRole('button', { name: 'Submit' }).click();Isso torna o teste mais lento e ainda flaky. Às vezes 2 segundos não são suficientes.
A correção certa: esperar pela condição específica que precisa ser verdadeira antes da ação.
// Esperar um indicador de carregamento desaparecer
await page.getByTestId('loading-spinner').waitFor({ state: 'hidden' });
await page.getByRole('button', { name: 'Submit' }).click();
// Esperar um botão ficar habilitado
await page.getByRole('button', { name: 'Submit' }).waitFor({ state: 'visible' });
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
await page.getByRole('button', { name: 'Submit' }).click();
// Esperar uma requisição de rede terminar
await page.waitForResponse(resp =>
resp.url().includes('/api/items') && resp.status() === 200
);O auto-waiting integrado do Playwright cuida da maioria dos casos automaticamente. Quando o auto-waiting não é suficiente, espere pela coisa específica, não por uma duração fixa.
Corrigindo poluição de testes
Se os testes passam individualmente mas falham quando rodam juntos, o problema é quase certamente estado vazando entre testes.
Verifique essas fontes de poluição:
Armazenamento do navegador. Se um teste escreve emlocalStorage ou sessionStorage e outro lê, você tem poluição. O Playwright cria um contexto de navegador novo para cada arquivo de teste por padrão, mas testes dentro do mesmo arquivo compartilham contexto por padrão em algumas configurações.
// Limpar o armazenamento antes de cada teste no arquivo
test.beforeEach(async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
});test.afterEach(async ({ request }) => {
// Deletar o registro de teste criado durante o teste
await request.delete('https://lab.becomeqa.com/api/items/test-item-id');
});--repeat-each=3 para ver se são estáveis quando repetidos. Um teste que falha na segunda execução está vazando estado. npx playwright test --repeat-each=3 tests/login.spec.tsCorrigindo conflitos de execução paralela
O Playwright roda testes em paralelo por padrão em múltiplos workers. Se dois testes tentam modificar o mesmo registro ou usar a mesma conta de usuário simultaneamente, eles conflitam.
A correção depende da situação:
Use dados de teste únicos por teste. Em vez de sempre usaradmin@becomeqa.com, gere um identificador único para cada execução de teste:
const uniqueId = Date.now();
const testEmail = `test-${uniqueId}@example.com`;// No topo do arquivo
test.describe.configure({ mode: 'serial' });Isso roda todos os testes no arquivo sequencialmente, prevenindo conflitos.
Use dados de teste separados por worker. O Playwright passa umworkerIndex para fixtures:
const workerEmail = `test-worker-${workerInfo.workerIndex}@example.com`;Use retries com cuidado
O Playwright suporta retries automáticos para testes flaky:
// playwright.config.ts
export default defineConfig({
retries: process.env.CI ? 2 : 0,
});Retries no CI mascaram problemas em vez de corrigi-los, mas são uma ferramenta prática para flakiness genuína de infraestrutura: timeouts de rede e variação da máquina de CI.
A regra: retries são aceitáveis para flakiness de infraestrutura. Não são aceitáveis como substituto para corrigir problemas reais de timing ou isolamento.
retries: 3 sem investigar por que os testes falham é como você acaba com uma suite que demora 3x mais para rodar e ainda não tem nenhum teste em que você realmente confia.Quarentene testes persistentemente flaky
Se um teste é flaky e você não consegue corrigi-lo imediatamente, coloque-o em quarentena. Não deixe na suite principal falhando aleatoriamente.
test.skip('checkout flow completes successfully', async ({ page }) => {
// Flaky due to payment API timeouts — tracked in JIRA-1234
// TODO: mock the payment API response instead of hitting the real one
});Um teste pulado com um comentário é infinitamente melhor do que um teste flaky que treina o time a ignorar builds vermelhos.
Um fluxo sistemático de debugging
Quando você encontrar um teste flaky, siga essa ordem:
1. Capture o trace: rode com retries: 1 e trace: 'on-first-retry', veja o ponto exato de falha
2. Rode 10 vezes: npx playwright test --repeat-each=10 tests/your.spec.ts, veja com que frequência falha
3. Rode em isolamento: npx playwright test tests/your.spec.ts, se passar sozinho, é poluição de teste
4. Rode com headed: npx playwright test --headed --slow-mo=500, assista falhar em câmera lenta
5. Verifique a aba de rede no trace: as requisições estão falhando ou com timeout?
6. Adicione waits explícitos para a condição específica que precisa ser verdadeira antes da ação que falha
7. Verifique estado compartilhado: o que o teste anterior faz?
A maioria dos testes flaky é resolvida no passo 3 ou no passo 6.
FAQ
Como sei se um teste é genuinamente flaky ou se pegou um bug real?Rode 10 vezes no mesmo commit. Se falha 2 de 10, é flaky. Se falha 10 de 10, pegou um bug.
Meu teste só falha no CI, nunca localmente. Por quê?Máquinas de CI são mais lentas e têm menos memória. Problemas de timing que são invisíveis localmente aparecem sob carga. Rode localmente com --slow-mo=500 para simular uma máquina mais lenta. Também verifique se o CI usa uma base URL ou variáveis de ambiente diferentes.
test.fixme ou test.skip para testes flaky conhecidos?
test.skip exclui o teste completamente. test.fixme o marca como quebrado mas ainda o roda. O teste é esperado para falhar, e vira falha se começar a passar (o que te alerta para verificá-lo). Para testes flaky conhecidos que precisam de correção, test.fixme é a escolha mais honesta.
O trace mostra que o elemento estava visível mas o clique ainda falhou. O que aconteceu?
O elemento estava visível mas provavelmente coberto por outro elemento (um modal, um tooltip, um header fixo). Verifique isVisible() vs isInViewport(). Pode ser necessário rolar até o elemento primeiro: await locator.scrollIntoViewIfNeeded().