Programação Por Equipe DevBras

Capa do artigo: Teste que só passa sozinho: como achar e cortar o estado compartilhado

Teste que só passa sozinho: como achar e cortar o estado compartilhado

Descubra qual teste está vazando estado para o vizinho, rode a suíte em ordem aleatória e faça cada teste preparar e limpar o próprio dado.

Na sua máquina, pedidos.test.js passa sozinho. No CI, rodando com o resto da suíte, ele falha dizendo que o cliente "maria@exemplo.com" já existe. Ou o contrário: sozinho ele falha, porque dependia de uma linha que clientes.test.js inseria antes. A resposta curta: o teste está lendo ou deixando estado que não é dele. A correção é fazer cada teste criar o dado que usa, com identificador próprio, e devolver o ambiente ao estado limpo no fim, de preferência com uma transação desfeita. Para provar que funcionou, rode a suíte em ordem aleatória.

Isso não é "CI instável". A ordem dos testes nunca foi contrato. Quando ela muda, porque alguém renomeou um arquivo ou o executor passou a rodar em paralelo, o acoplamento que já existia aparece.

De onde vem o estado que vaza

Um teste bem isolado começa e termina no mesmo mundo. Ele vaza quando altera algo que sobrevive ao fim dele e que outro teste lê. Os suspeitos de sempre são poucos:

  • Banco de dados compartilhado: linhas inseridas e não removidas, sequências que avançaram, um registro "admin" que um teste apaga e outro usa.
  • Variáveis de módulo: um cache em memória, um singleton de configuração, um array exportado que os testes alteram.
  • Relógio e ambiente: relógio falso não restaurado, process.env alterado, fuso trocado.
  • Arquivos e portas: um arquivo temporário com nome fixo, um servidor de teste que ficou escutando na mesma porta.
  • Mocks globais: um módulo substituído num teste e nunca devolvido ao original.

O padrão é sempre o mesmo: o teste A deixa uma marca, o teste B depende dela (ou tropeça nela), e a dependência só funciona quando A roda antes de B. Por isso o sintoma é intermitente. Ele depende da ordem e, em execução paralela, de quem chega primeiro.

Como encontrar o par acoplado

Antes de corrigir, descubra quem pisa em quem. Esse processo leva minutos e evita que você "conserte" o teste errado.

  1. Rode o teste que falha sozinho. Se passa sozinho e falha na suíte, alguém antes dele deixa sujeira. Se falha sozinho e passa na suíte, ele depende de algo que outro teste cria.
  2. Rode em ordem aleatória várias vezes. No Jest, a opção --randomize embaralha a ordem e mostra a semente usada; no Vitest, a configuração sequence.shuffle faz o mesmo; no pytest, o plugin pytest-randomly cumpre esse papel. Anote a semente da execução que falhou para repetir a mesma ordem.
  3. Reduza pela metade. Rode o teste que falha junto com metade dos arquivos que vêm antes dele. Se falhar, o culpado está nessa metade; se não, está na outra. Em poucas rodadas você chega a um par.
  4. Leia o que o culpado altera e não desfaz. Quase sempre é uma linha no banco, uma variável de módulo ou um mock global.

Rodar de novo até passar é exatamente o que você não deve fazer. O post teste intermitente não se resolve clicando em rodar de novo trata do lado do processo: quem é dono do teste e quando colocá-lo em quarentena. Aqui o foco é a correção técnica.

Como isolar o banco de dados

No nosso exemplo, a loja tem dois arquivos de teste que usam o mesmo PostgreSQL. clientes.test.js cria "maria@exemplo.com" e não apaga. pedidos.test.js também cria "maria@exemplo.com" e espera sucesso. Quando a ordem muda, a restrição de e-mail único derruba o segundo.

Opção 1: uma transação por teste

Cada teste abre uma transação, faz o que precisa e, no fim, desfaz tudo com ROLLBACK. O banco volta exatamente ao estado anterior, e não sobra nada para o vizinho.

// pedidos.test.js
const { test, beforeEach, afterEach, after } = require('node:test');
const assert = require('node:assert/strict');
const { Pool } = require('pg');
const { criarCliente, criarPedido } = require('./loja');

const pool = new Pool({ connectionString: process.env.DATABASE_URL_TESTE });
let db;

beforeEach(async () => {
  db = await pool.connect();
  await db.query('BEGIN');
});

afterEach(async () => {
  await db.query('ROLLBACK');
  db.release();
});

after(() => pool.end());

test('pedido novo começa como pendente', async () => {
  const cliente = await criarCliente(db, { email: 'maria@exemplo.com' });
  const pedido = await criarPedido(db, { clienteId: cliente.id, totalCentavos: 15990 });
  assert.equal(pedido.status, 'pendente');
});

O detalhe que faz isso funcionar: o código testado precisa usar a mesma conexão que abriu a transação. Por isso criarCliente e criarPedido recebem db como parâmetro. Se o código pega outra conexão do pool por conta própria, ele não enxerga a transação e grava de verdade. Essa técnica também não serve quando o próprio código testado faz COMMIT ou abre conexões em paralelo; nesses casos, use a opção seguinte.

Opção 2: dados únicos e limpeza explícita

Quando a transação não cabe, por exemplo num teste de ponta a ponta que passa por uma API HTTP, cada teste gera seus próprios identificadores e apaga o que criou.

const { randomUUID } = require('node:crypto');

function emailUnico() {
  return 'teste-' + randomUUID() + '@exemplo.com';
}

Com e-mail único, dois testes nunca disputam o mesmo cliente, mesmo rodando em paralelo. A limpeza fica num afterEach que apaga pelos identificadores criados naquele teste, não com um DELETE geral que pode atingir dados de outro teste em execução.

Opção 3: banco descartável

Para suítes de integração mais pesadas, suba um banco novo por execução, com o esquema aplicado pelas mesmas migrações de produção. Ferramentas como Testcontainers fazem isso em várias linguagens. É mais lento que a transação, mas elimina sobras de execuções anteriores.

Estado em memória também vaza

Nem todo vazamento passa pelo banco. Um módulo que guarda cache em variável de topo mantém o valor entre testes do mesmo arquivo, e às vezes entre arquivos, dependendo do executor. A saída é a mesma: expor uma forma de criar uma instância nova por teste, em vez de exportar um objeto único já pronto. Se o módulo precisa ser global, ofereça uma função de reinício e chame no beforeEach.

Com process.env, guarde o valor original antes do teste e devolva depois. Com mocks e relógio falso, use as funções de restauração do executor num afterEach, não no fim do corpo do teste, porque o fim do corpo não roda quando uma asserção falha. O mesmo cuidado com o relógio aparece em teste que depende do relógio real.

Erros comuns

  • Usar um usuário fixo de seed para tudo. Basta um teste alterar a senha ou apagar esse usuário para metade da suíte cair, sem relação com o que mudou.
  • Limpar no fim do corpo do teste. Se uma asserção falha antes, a limpeza não roda e a falha se espalha para os seguintes.
  • Apagar a tabela inteira entre testes paralelos. Um teste limpa o dado que o outro estava usando naquele instante, e o sintoma parece aleatório.
  • Usar transação com código que abre a própria conexão. O teste parece isolado, mas o código grava fora da transação e deixa sobra.
  • Desligar o paralelismo para "resolver". A suíte fica mais lenta e o acoplamento continua lá, esperando a próxima mudança de ordem.
  • Depender de dados preparados por um arquivo de fixture enorme. Ninguém sabe qual teste usa qual linha; o post sobre fixture gigante mostra como montar só o que o teste precisa.

Checklist

  • Cada teste passa rodando sozinho e passa rodando com a suíte inteira.
  • A suíte passa em ordem aleatória em pelo menos cinco execuções seguidas.
  • Testes de banco usam transação desfeita ou identificadores únicos.
  • O código testado recebe a conexão em vez de pegar uma por conta própria.
  • Limpeza de mocks, relógio e variáveis de ambiente fica em afterEach.
  • Nenhum teste depende de um registro criado por outro arquivo.
  • A semente de uma execução aleatória que falhou é registrada para reprodução.

Rode a sua suíte agora com a ordem embaralhada três vezes seguidas: ela passa nas três, ou alguma semente mostra qual teste estava encostado no vizinho?

Avalie este artigo

Referência: https://martinfowler.com/bliki/TestDouble.html