ConstructiCat Logo
CodeBust.
Browse section ▾

Teste Errático (Instável).

Um teste errático (instável) passa e falha de forma intermitente sobre o mesmo código porque seu resultado depende de tempo, ordenação, estado compartilhado ou outros fatores não determinísticos, em vez do comportamento sob teste.

##Signs and Symptoms

Um teste que está verde em uma execução e vermelho na seguinte sem nenhuma alteração de código é errático. Você o reconhece tanto pelo comportamento humano ao seu redor quanto pelo código: desenvolvedores reexecutam a CI "para fazer passar," adicionam @Flaky/retry(3) ou colocam o teste em quarentena em vez de corrigi-lo.

Sinais comuns no código e no padrão de falha:

  • Tempo/assíncrono: "esperas" com sleep/setTimeout, promises não aguardadas ou asserções que correm contra o sistema sob teste. As falhas se correlacionam com a velocidade da máquina ou a carga da CI.
  • Dependência de ordem: o teste passa isoladamente mas falha na suíte completa, ou vice-versa. Embaralhar a ordem dos testes muda o resultado.
  • Estado mutável compartilhado: coleções estáticas, uma linha de banco de dados reaproveitada, um singleton ou uma fixture mutada por um teste anterior.
  • Entradas não determinísticas: Date.now()/new Date(), Math.random(), locale/fuso horário, ordem de iteração de hash-map ou IDs gerados automaticamente.
  • Recursos externos: rede, sistema de arquivos ou relógio reais. As falhas parecem timeouts ou "connection refused," não divergências de asserção.
  • Asserções condicionais: expect escondido dentro de if/catch/callbacks, então o teste passa silenciosamente quando o ramo nunca é executado.
// Smell: uma "espera" fixa, um array compartilhado e um relógio real
const created = [];                       // compartilhado entre testes → testes interagentes

test('shows a fresh receipt', async () => {
  render(<Checkout />);
  fireEvent.click(screen.getByText('Pay'));
  await new Promise(r => setTimeout(r, 300));   // torcer para que a requisição tenha terminado
  created.push('order-1');                       // vaza para testes posteriores
  expect(screen.getByRole('status'))
    .toHaveTextContent(`Paid ${new Date().toISOString()}`); // muda a cada execução
});

Isso mapeia diretamente para os sub-smells do Teste Errático de Meszaros: Testes Interagentes / Guerra de Execução de Testes (estado compartilhado), Teste Solitário (só roda depois de outro), Otimismo de Recursos (assume que um recurso externo está disponível), Vazamento de Recursos (não faz limpeza) e Teste Não Determinístico / Irrepetível (tempo, aleatoriedade, assíncrono).

##Reasons for the Problem

Por que isso acontece

  • Tempo implícito. Código assíncrono é "sincronizado" com sleeps fixos ou simplesmente sem aguardar. O atraso é um chute: suficiente hoje, curto demais sob carga amanhã.
  • Acoplamento oculto. Os testes compartilham um banco de dados, um singleton, variáveis em nível de módulo ou arquivos em disco. Os efeitos colaterais de um teste tornam-se as precondições de outro (os Testes Interagentes / Guerra de Execução de Testes de Meszaros), então os resultados dependem da ordem e do que rodou em paralelo.
  • Entradas não determinísticas se infiltram. O horário real do relógio, Math.random(), locale/fuso horário e coleções sem ordem variam entre execuções e máquinas.
  • Dependência otimista do ambiente. Os testes acessam uma rede/serviço ao vivo ou assumem que um arquivo existe (Otimismo de Recursos) e nunca liberam o que adquirem (Vazamento de Recursos).
  • Asserções que podem ser puladas. Colocar expect em um condicional ou em um .catch significa que a verificação pode nunca ser executada, então o teste "passa" por acidente.

Por que isso é prejudicial

  • Falsa confiança e defeitos mascarados. Um teste instável pode falhar por motivos não relacionados a produção, e uma regressão real pode se esconder atrás de uma falha que todos assumem ser "só instabilidade." Você não consegue mais distinguir sinal de ruído.
  • Erosão da confiança. Quando uma suíte é conhecida por ser instável, os desenvolvedores ignoram builds vermelhos e reexecutam por reflexo, o que treina a equipe a desconsiderar a suíte de testes por completo.
  • Tempo desperdiçado e pipelines quebrados. Retentativas, reexecuções e investigações do tipo "sou eu ou é o teste?" atrasam todo mundo e bloqueiam o CI/CD por não-problemas.
  • Baixa manutenibilidade. Testes erráticos são difíceis de depurar porque a falha não é reproduzível; eles tendem a ser desativados em vez de corrigidos, reduzindo silenciosamente a cobertura real.

##Treatment

Trate a instabilidade como um defeito no teste, não como uma peculiaridade a ser contornada com retentativas. Reexecuções servem para detectar a instabilidade, nunca para escondê-la. Coloque o teste em quarentena se ele bloquear o pipeline, e então investigue a causa raiz.

1. Substitua sleeps por espera baseada em condição. Faça polling do estado que você realmente quer em vez de chutar uma duração.

// antes — atraso fixo com condição de corrida
fireEvent.click(screen.getByText('Pay'));
await new Promise(r => setTimeout(r, 300));
expect(screen.getByRole('status')).toBeInTheDocument();

// depois — aguardar a condição e então verificar
fireEvent.click(screen.getByText('Pay'));
expect(await screen.findByRole('status')).toBeInTheDocument();

2. Torne as entradas determinísticas. Injete o relógio ou use fake timers; semeie ou faça stub da aleatoriedade; fixe locale/fuso horário.

// antes: depende da data real
expect(label).toBe(`Paid ${new Date().toISOString()}`);

// depois: congelar o tempo (vitest/jest)
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-06-15T00:00:00Z'));

3. Isole cada teste. Dê a cada teste fixtures novas, redefina o estado compartilhado (banco de dados, singletons, globais de módulo) em beforeEach/afterEach e evite variáveis mutáveis em nível de módulo. Execute a suíte em ordem aleatória (por exemplo, --seed/randomize do Jest, MethodOrderer.Random do JUnit) para revelar dependências de ordenação cedo.

4. Faça stub de recursos externos. Faça mock de rede, sistema de arquivos e chamadas de sistema para que o teste nunca dependa de um serviço estar no ar (cura o Otimismo de Recursos); sempre libere/limpe os recursos adquiridos (cura o Vazamento de Recursos).

5. Aguarde todo o trabalho assíncrono e verifique incondicionalmente. Retorne/aguarde promises e utilitários de teste assíncronos; mova os efeitos colaterais para fora dos callbacks de waitFor para que rodem uma única vez; nunca enterre expect dentro de if/catch.

6. Verifique a correção. Execute o teste agora determinístico muitas vezes (e sob carga / ordem embaralhada) para confirmar a estabilidade antes de tirá-lo da quarentena.

##Detected by