Trabalho Por Equipe DevBras

Capa do artigo: Teste flaky: por que rerodar o CI piora tudo e o que fazer no lugar

Teste flaky: por que rerodar o CI piora tudo e o que fazer no lugar

Como medir a instabilidade de um teste, tirá-lo da trava do merge sem esquecê-lo e achar a causa real: tempo, ordem, recurso compartilhado ou espera mal feita.

Terça, 16h. O pipeline da loja fica vermelho no teste checkout aplica cupom. Alguém clica em "Re-run", fica verde, o merge entra. Na quinta, o mesmo teste falha por um bug real no cupom, alguém clica em "Re-run" de novo, passa por sorte, e o bug vai para produção. Rerodar não conserta o teste flaky: ensina o time a não acreditar no vermelho.

A resposta curta: meça quantas vezes o teste falha, tire-o da trava do merge com dono e prazo, investigue a causa (quase sempre tempo, ordem, recurso compartilhado ou dependência externa) e só devolva o teste à fila obrigatória quando ele passar dezenas de vezes seguidas. Enquanto isso, nada de aumentar o sleep até "passar na maioria".

Por que um teste passa e falha sem o código mudar

Um teste é determinístico só quando tudo de que ele depende também é. Na prática, ele depende de coisas que o código do teste não controla:

  • Tempo: relógio real, fuso, virada do dia, timeouts curtos, sleep fixo esperando algo assíncrono terminar.
  • Ordem: um teste deixa dado no banco ou numa variável global, e outro só passa se rodar depois (ou antes) dele.
  • Recurso compartilhado: mesma porta, mesmo arquivo temporário, mesma tabela usada por testes em paralelo.
  • Concorrência: uma promessa sem await, um evento que às vezes chega antes da asserção.
  • Mundo externo: API de terceiro, DNS, fila real, máquina de CI mais lenta naquele horário.

O problema cresce com o tamanho da suíte. Se você tem 20 testes que falham sozinhos 2% das vezes cada, a chance de todos passarem numa rodada é 0,98 elevado a 20, cerca de 67%. Um terço das execuções fica vermelho sem nenhum bug. Nesse ponto, ninguém lê mais o erro, e o vermelho de verdade passa junto.

O próprio time de testes do Google, no texto que inspirou este guia, relatou que uma parcela grande dos testes da empresa tinha algum grau de instabilidade e que isso custava tempo de engenharia e confiança no CI. Não é problema de time pequeno ou desorganizado. É o estado natural de uma suíte que ninguém vigia.

Como tratar um teste flaky, passo a passo

  1. Confirme que é instabilidade, não bug. Rode o teste sozinho muitas vezes no mesmo commit. Se falha às vezes sem mudança de código, é flaky.
  2. Meça a taxa. "Falha de vez em quando" não ajuda ninguém. "Falhou 4 em 50" ajuda a priorizar e a saber quando o conserto funcionou.
  3. Abra um chamado com dono e data. Inclua o log da falha, a taxa medida e o commit. Sem dono, a quarentena vira cemitério.
  4. Tire da trava do merge, não da suíte. O teste continua rodando num job que não bloqueia, para você ver se ele melhora ou piora.
  5. Ache a causa pela categoria. Use a lista acima como roteiro: tempo, ordem, recurso, concorrência, mundo externo.
  6. Conserte e prove. Rode de novo as 50 vezes. Só então devolva o teste ao job obrigatório.

Medindo a taxa de falha

No exemplo da loja, o teste mora em tests/checkout.test.js e a suíte usa Jest. Um laço simples em bash já mostra a taxa:

falhas=0
for i in $(seq 1 50); do
  if ! npx jest tests/checkout.test.js --silent > /dev/null 2>&1; then
    falhas=$((falhas + 1))
    echo "falhou na rodada $i"
  fi
done
echo "total: $falhas falhas em 50"

Se sozinho ele passa 50 de 50, mas falha na suíte completa, a suspeita muda: é ordem ou recurso compartilhado. Rode a suíte inteira em ordem aleatória (o Jest tem a opção --randomize nas versões recentes; no pytest, o plugin pytest-randomly faz isso) e anote a semente que reproduz a falha.

Quarentena sem esquecer o teste

Mova o teste para uma pasta de quarentena e crie um job separado que roda essa pasta sem bloquear o merge. No GitHub Actions, fica assim:

jobs:
  testes:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx jest --testPathIgnorePatterns=tests/quarentena

  quarentena:
    runs-on: ubuntu-latest
    continue-on-error: true
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx jest tests/quarentena

Na proteção de branch, marque só o job testes como obrigatório. O job quarentena continua visível, e a falha dele aparece no histórico. Escreva no topo do arquivo em quarentena o link do chamado e a data combinada para voltar.

Consertando a causa mais comum: a espera fixa

No caso da loja, o culpado era este trecho:

test('pedido muda para pago', async () => {
  await api.post('/pedidos/42/pagar');
  await new Promise((r) => setTimeout(r, 500));
  const pedido = await api.get('/pedidos/42');
  expect(pedido.status).toBe('pago');
});

O pagamento é processado por um worker. Na máquina da pessoa, 500 ms sobram. No runner de CI compartilhado, às vezes não bastam. Aumentar para 2000 ms deixa a suíte mais lenta e só empurra o problema. O certo é esperar pela condição, com prazo máximo:

async function esperarAte(condicao, { timeoutMs = 5000, intervaloMs = 50 } = {}) {
  const limite = Date.now() + timeoutMs;
  while (Date.now() < limite) {
    if (await condicao()) return;
    await new Promise((r) => setTimeout(r, intervaloMs));
  }
  throw new Error('condição não atingida em ' + timeoutMs + ' ms');
}

test('pedido muda para pago', async () => {
  await api.post('/pedidos/42/pagar');
  await esperarAte(async () => {
    const pedido = await api.get('/pedidos/42');
    return pedido.status === 'pago';
  });
});

No caminho feliz, o teste termina assim que o status muda, quase sempre bem antes dos 500 ms antigos. No caminho ruim, ele falha com uma mensagem clara em vez de uma asserção confusa. Depois do conserto, o laço de 50 rodadas deu 50 de 50, e o teste voltou ao job obrigatório.

Para as outras causas, há guias específicos: teste que depende do relógio real, teste que só passa sozinho e mock que não parece o serviço real.

Uma política de time, não um esforço heroico

O conserto técnico resolve um teste. A política impede que o próximo apodreça. Combine três regras simples e escreva no repositório:

  • Quem viu o teste falhar sem motivo abre o chamado, mesmo que não vá consertar.
  • Rerun automático, se existir, registra cada falha. Teste que só passa na segunda tentativa é contado como instável, não como verde.
  • A pasta de quarentena tem limite. Se passar de, digamos, cinco testes, o time para e conserta antes de criar funcionalidade nova.

O objetivo é que vermelho volte a significar "tem problema aqui". Um teste em que o time não acredita é pior que teste nenhum, porque gasta tempo e ainda treina as pessoas a ignorar o alarme.

Erros comuns

  • Aumentar o sleep até passar. A suíte fica mais lenta a cada ajuste e o teste volta a falhar no dia em que o runner estiver mais carregado.
  • Apagar o teste "porque é instável". A funcionalidade fica sem cobertura e o bug que o teste às vezes pegava passa a chegar em produção.
  • Quarentena sem dono nem data. Vira uma pasta de testes que ninguém olha, e a cobertura real cai em silêncio.
  • Rerun automático que esconde a falha. O painel mostra 100% verde enquanto a taxa de instabilidade cresce sem ninguém medir.
  • Investigar só na máquina local. O teste passa 200 vezes no notebook rápido e continua falhando no runner, que tem menos CPU e roda jobs em paralelo.
  • Culpar a infraestrutura de cara. Às vezes é a rede do CI, mas na maioria dos casos o teste depende de algo que deveria estar controlado.

Checklist

  • O teste instável tem chamado aberto com log, taxa medida e dono.
  • Existe um job de quarentena que roda e não bloqueia o merge.
  • Só os jobs confiáveis estão marcados como obrigatórios na proteção de branch.
  • Nenhum teste espera com sleep fixo por algo assíncrono.
  • A suíte já rodou em ordem aleatória pelo menos uma vez.
  • Testes paralelos não compartilham porta, arquivo nem tabela sem isolamento.
  • O conserto foi provado com dezenas de rodadas seguidas antes de sair da quarentena.

Abra o histórico do seu CI desta semana: quantas vezes alguém clicou em rodar de novo em vez de ler o erro, e qual teste aparece em todas elas?

Avalie este artigo

Referência: https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html