ConstructiCat Logo
CodeBust.
Browse section ▾

Dependência de Ordem dos Testes.

Um teste passa ou falha dependendo de quais outros testes rodaram antes dele, porque os testes vazam e dependem de estado mutável compartilhado em vez de cada um montar e desmontar a sua própria fixture.

##Signs and Symptoms

O resultado de um teste depende da ordem em que a suíte é executada, e não apenas do código sob teste. Cada teste deveria ser autocontido, mas aqui um teste depende silenciosamente de efeitos colaterais deixados por outro.

Sinais reveladores:

  • Um teste passa na suíte completa, mas falha quando executado sozinho (via .only, um filtro por nome ou executando apenas aquele arquivo). Meszaros chama isso de Lonely Test.
  • Os testes quebram quando você embaralha, faz sharding ou paraleliza a execução, ou depois de atualizar um executor que muda a ordem padrão.
  • Excluir, pular ou reordenar um teste faz com que um teste aparentemente não relacionado falhe.
  • Uma falha dispara uma cascata de falhas subsequentes (os Interacting Tests de Meszaros; a Test Run War de van Deursen, quando executores paralelos colidem em torno de uma fixture compartilhada).
  • Os testes ficam intermitentemente instáveis (flaky) sem nenhuma mudança no código — a dependência de ordem é uma das causas raízes mais comuns de testes instáveis.

O indício estrutural é o estado mutável compartilhado que é lido entre os testes: variáveis de escopo de módulo/static/globais, um beforeAll que popula o estado uma única vez, ou um recurso externo não reiniciado (linhas de banco de dados, arquivos, caches, variáveis de ambiente, timers falsos, mocks).

let users = []; // estado compartilhado no escopo do módulo

test('creates a user', () => {
  users.push({ id: 1, name: 'Ada' });
  expect(users).toHaveLength(1);
});

// Verde apenas porque o teste acima rodou primeiro e mutou `users`.
// Execute este teste sozinho, ou embaralhe a ordem, e ele falha.
test('finds the created user', () => {
  expect(users.find(u => u.name === 'Ada')).toBeDefined();
});

##Reasons for the Problem

Por que acontece

  • Uma Shared Fixture (montada uma única vez em beforeAll, uma variável com escopo de módulo, um static de classe ou um banco de dados/arquivo real) é mutada pelos testes e nunca reiniciada entre eles, de modo que cada teste herda as sobras do anterior.
  • Conveniência e velocidade: as pessoas reutilizam uma configuração custosa ou "constroem sobre" os dados do teste anterior para evitar recriá-los (às vezes formalizado como Chained Tests).
  • A dependência é acidental e invisível — nada no código do teste declara "execute-me depois daquele", então ela sobrevive até que a ordem mude.

Por que é prejudicial

  • Confiabilidade / falsa confiança. A suíte fica verde por sorte na ordenação. Reordene, paralelize ou execute um subconjunto e os testes falham (ou, pior, uma regressão real se esconde porque um teste anterior acabou deixando um estado "correto" para trás). Testes dependentes de ordem são uma das principais fontes de testes instáveis (flaky).
  • Manutenibilidade. Você não consegue executar, depurar ou reexecutar isoladamente um único teste que falha — o teste pré-requisito precisa rodar primeiro. Adicionar, remover ou reordenar testes provoca ação fantasmagórica à distância.
  • Legibilidade. Um teste deixa de documentar um único comportamento com entradas explícitas; entendê-lo exige ler o que quer que tenha rodado antes dele (um Mystery Guest escondido na ordem de execução).
  • Bloqueia o paralelismo e a seleção. O sharding de testes, os executores paralelos e as ferramentas de impacto/seleção de testes todos pressupõem independência; o acoplamento por ordem os torna inseguros.

##Treatment

Torne cada teste independente: ele monta tudo de que precisa, faz as asserções e limpa, de modo que produz o mesmo resultado em qualquer ordem, sozinho ou em uma suíte.

  1. Dê a cada teste uma Fresh Fixture. Mova a configuração compartilhada de beforeAll para beforeEach (ou construa-a dentro do teste), para que o estado seja reconstruído a cada teste em vez de acumulado.
  2. Elimine o estado mutável compartilhado. Não leia/escreva variáveis de escopo de módulo, static ou globais entre testes. Construa os objetos localmente; passe os dados explicitamente.
  3. Reinicie os recursos externos no teardown. Faça rollback do banco de dados (uma transação por teste) ou use dados únicos por teste; exclua arquivos temporários; restaure variáveis de ambiente, globais e timers falsos; limpe os mocks (jest.clearAllMocks() / vi.restoreAllMocks(), jest.resetModules()). Prefira um teardown automático/garantido em vez de limpeza manual.
  4. Prove a independência embaralhando a ordem. Esse é o verdadeiro detector — é uma propriedade dinâmica, não algo que um linter consiga ver:
    • Jest: --randomize / randomize: true.
    • Vitest: sequence.shuffle (na configuração ou --sequence.shuffle).
    • pytest: pytest-randomly ou pytest-random-order.
    • Maven Surefire: -Dsurefire.runOrder=random. Execute também um subconjunto/um único teste na CI, para que os Lonely Tests apareçam.
  5. Se a configuração compartilhada for genuinamente necessária (uma fixture custosa, somente leitura), torne-a imutável e compartilhada apenas para leitura, ou use uma suíte de Chained Test deliberadamente documentada como último recurso — nunca uma dependência acidental. Observe que a regra no-hooks do eslint-plugin-jest/eslint-plugin-vitest pode desencorajar os hooks de setup/teardown que tendem a promover estado compartilhado, mas ela não detecta a dependência de ordem em si.
// Antes — dependente de ordem: o teste 2 precisa das sobras do teste 1
let users = [];
test('creates a user', () => {
  users.push({ id: 1, name: 'Ada' });
  expect(users).toHaveLength(1);
});
test('finds the created user', () => {
  expect(users.find(u => u.name === 'Ada')).toBeDefined();
});

// Depois — cada teste é dono da sua própria fixture
function makeRepo(seed = []) {
  return { users: [...seed] };
}

test('creates a user', () => {
  const repo = makeRepo();
  repo.users.push({ id: 1, name: 'Ada' });
  expect(repo.users).toHaveLength(1);
});

test('finds an existing user', () => {
  const repo = makeRepo([{ id: 1, name: 'Ada' }]); // monta a sua própria pré-condição
  expect(repo.users.find(u => u.name === 'Ada')).toBeDefined();
});