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:
expectescondido dentro deif/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
expectem um condicional ou em um.catchsignifica 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
- sonar java:S5973 — Os testes devem ser estáveis
- sonar java:S2925 — "Thread.sleep" não deve ser usado em testes
- eslint-jest no-conditional-expect — no-conditional-expect
- eslint-vitest no-conditional-expect — no-conditional-expect
- eslint-testing-library await-async-queries — await-async-queries
- eslint-testing-library await-async-utils — await-async-utils
- eslint-testing-library no-wait-for-side-effects — no-wait-for-side-effects