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-report

Clique 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: hidden

O 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 em localStorage 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();
  });
});

Estado do banco de dados. Se seus testes criam registros e não os limpam, testes que rodam depois veem dados inesperados.

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');
});

Estado global de teste. Se você usa variáveis globais nos seus arquivos de teste para compartilhar dados entre testes, não faça. Cada teste deve ser autocontido.
Rode seus testes com --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.ts

Corrigindo 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 usar admin@becomeqa.com, gere um identificador único para cada execução de teste:

const uniqueId = Date.now();
const testEmail = `test-${uniqueId}@example.com`;

Isole testes paralelos. Agrupe testes que conflitam no mesmo arquivo e configure esse arquivo para rodar com um único worker:

// 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 um workerIndex 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.

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

Devo usar 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().

→ Veja também: Estratégias de Espera no Playwright: Sem sleep() | Playwright Trace Viewer: Depure Testes com Falha Como um Profissional | Isolamento de Testes: Por que Cada Teste Playwright Deve Ser sem Estado | Testes Instáveis: Por que Acontecem e Como Eliminá-los