Uma tela mostra dados antigos quando duas requisições terminam fora da ordem em que começaram. O teste costuma passar, porque a rede foi rápida naquele dia. Depois alguém o repete várias vezes, torcendo para que a corrida apareça.

Esse teste não escolhe a condição que você precisa investigar. Para testar uma condição de corrida em JavaScript, mantenha as operações assíncronas pendentes e libere cada uma na ordem suspeita. A asserção deve verificar a invariável do sistema, não apenas o fato de que duas funções foram chamadas.

Eu executei a fixture deste artigo com Node.js v25.5.0. Ela usa node:test, Promises adiadas e estado em memória. O exemplo prova uma regra de atualização dentro de um processo. Ele não prova isolamento de banco, lock distribuído, entrega de fila ou comportamento de um navegador real.

Diagrama mostra a segunda Promise resolvendo primeiro e o resultado antigo sendo bloqueado por um teste de condição de corrida.

Resposta curta

  • Não use setTimeout aleatório para tentar reproduzir a corrida.
  • Crie Promises pendentes e controle quando cada operação termina.
  • Escreva a invariável que deve continuar verdadeira depois de cada ordem relevante.
  • Repita a prova na fronteira real quando o risco estiver no banco, na fila ou no navegador.

O que um teste de condição de corrida precisa provar?

Ele precisa provar uma afirmação sobre uma ordem possível. Por exemplo: quando o usuário faz duas buscas, a resposta da busca mais recente pode atualizar a tela, mas uma resposta antiga não pode sobrescrevê-la depois. O teste deve forçar essa ordem e verificar o estado final.

Uma boa afirmação também diz o que não pode acontecer. "Duas Promises terminaram" descreve a preparação. "O resultado antigo não altera o estado atual" descreve a proteção. Essa diferença evita um teste verde que nunca alcança o efeito perigoso.

O runner nativo de testes do Node.js, consultado em 29/09/2026, trata uma Promise rejeitada retornada pelo teste como falha. Isso permite escrever uma fixture assíncrona comum, sem um callback separado para esconder a ordem da execução.

Por que Promise.all com atraso costuma falhar?

Promise.all espera todas as operações, mas não escolhe qual termina primeiro. Um setTimeout pode tornar uma tentativa mais provável, porém não transforma a sequência em uma prova. O teste ainda depende do relógio, da carga da máquina e do agendamento que o ambiente decidiu usar.

Veja o padrão que parece concorrente, mas não controla a corrida:

const result = await Promise.all([
  load('first'),
  load('second'),
]);

Esse código é correto quando você só precisa esperar pelos dois resultados. Ele não é suficiente quando a regra depende da ordem de conclusão. Repetir o caso em um loop também não resolve a lacuna. O loop pode exercitar a mesma ordem centenas de vezes e nunca alcançar a intercalação que causa o bug.

O artigo da Doppler sobre testar condições de corrida de forma confiável, consultado em 29/09/2026, usa o mesmo princípio em uma transação: o teste segura pontos intermediários até que as duas operações tenham feito a leitura necessária. O detalhe importante não é o banco específico. É tornar o escalonamento uma entrada controlável do teste.

Como controlar a conclusão das Promises?

Uma Promise adiada expõe seu resolve para o teste. A operação começa e fica parada até que o caso libere o ponto escolhido. Assim, o teste pode começar a busca antiga, começar a nova, resolver a nova e somente depois resolver a antiga.

function deferred() {
  let resolve;
  const promise = new Promise((value) => {
    resolve = value;
  });

  return { promise, resolve };
}

Agora crie uma função que mantém apenas o resultado da solicitação mais recente. O contador é uma versão local da intenção. Cada chamada captura sua versão antes de aguardar a leitura.

function createLatestLoader(read) {
  let version = 0;
  let current;

  return {
    load(key) {
      const requestVersion = ++version;

      return read(key).then((value) => {
        if (requestVersion === version) current = value;
        return value;
      });
    },
    current() {
      return current;
    },
  };
}

O guard não cancela a requisição antiga. Ele impede que uma conclusão atrasada altere o estado que já pertence a uma intenção mais recente. Em outro sistema, a proteção pode ser um AbortController, uma versão persistida ou uma transação. A invariável é a mesma somente se o contrato do sistema disser isso.

Como escrever o teste que reproduz a ordem perigosa?

O teste começa as duas operações sem aguardá-las imediatamente. Depois libera a segunda e espera que ela termine. Por fim, libera a primeira e verifica que o valor antigo não tomou o lugar do novo.

import test from 'node:test';
import assert from 'node:assert/strict';

test('uma resposta atrasada não sobrescreve o resultado mais recente', async () => {
  const first = deferred();
  const second = deferred();
  const loader = createLatestLoader((key) =>
    key === 'first' ? first.promise : second.promise,
  );

  const firstRun = loader.load('first');
  const secondRun = loader.load('second');

  second.resolve('new');
  await secondRun;
  first.resolve('old');
  await firstRun;

  assert.equal(loader.current(), 'new');
});

Eu rodei a lógica localmente com node --input-type=module e uma versão equivalente enviada pela entrada padrão. Em um arquivo de teste, o mesmo caso pode ser executado com node --test. O resultado foi uma aprovação. Isso confirma a fixture e o guard para essa ordem específica. Não é uma medição de latência nem uma prova de que o servidor remoto cancelou qualquer trabalho.

Para provar que o teste encontra o defeito, remova temporariamente o if (requestVersion === version) e atribua current = value sempre. A mesma ordem deve falhar porque a resposta antiga termina por último e sobrescreve o valor novo. Um teste que não fica vermelho quando a proteção é removida não está alcançando a linha que importa.

Quais ordens e falhas entram na matriz?

Comece pela ordem que motivou o bug, mas não pare nela. Uma matriz pequena pode cobrir a primeira operação terminando antes da segunda, a segunda terminando antes da primeira, uma falha na operação antiga e uma falha na nova. O objetivo é ligar cada cenário a uma decisão observável.

Cenário Preparação Invariável ou resultado
primeira termina antes libere first, depois second o estado final corresponde à regra definida pelo produto
segunda termina antes libere second, depois first o resultado antigo não sobrescreve o atual
leitura antiga falha rejeite first depois da segunda a falha velha não apaga o estado novo
leitura nova falha rejeite second antes da primeira o sistema mostra erro ou fallback sem aceitar dado obsoleto
cancelamento aborte a operação que perdeu relevância o teste diferencia cancelamento de falha inesperada

Não transforme toda combinação em teste unitário. A comparação entre testes unitários e de integração em Node.js ajuda a decidir qual colaborador precisa permanecer real. A fixture em memória prova a decisão do controlador. Um teste de integração prova a serialização, o banco, a fila ou o transporte que também participa da corrida. Quando a disputa envolve a mesma intenção de uma API, teste a idempotência sem duplicar efeitos na fronteira real.

Fake timers resolvem esse problema?

Às vezes. Fake timers são bons quando o comportamento depende de setTimeout, backoff, debounce ou intervalo. A documentação atual do Node.js sobre timers, consultada em 29/09/2026, descreve APIs de timer e a variante baseada em Promises. O guia para testar retry sem esperar o relógio real cobre essa fronteira.

Uma corrida entre duas respostas pode não depender de tempo. Ela depende de qual conclusão atravessa a próxima etapa primeiro. Nesse caso, controlar as Promises comunica melhor a causa e evita um teste que parece rápido, mas ainda esconde a ordem.

Se a função agenda um timer e também espera uma Promise externa, controle as duas coisas separadamente. Avançar o relógio não libera automaticamente toda a fila de Promises, e resolver a Promise não dispara um timer que ainda está pendente. Faça cada mudança de estado de forma explícita antes da asserção.

O que a fixture não prova em produção?

Uma fixture determinística não substitui o teste da fronteira onde a corrida causa dano. Um Map local não simula duas instâncias, uma transação concorrente, a visibilidade de uma mensagem ou o cancelamento de uma requisição no navegador.

Use a fixture para provar o algoritmo e o contrato de estado. Depois acrescente um teste com o armazenamento, o broker, o cliente HTTP ou o browser que realmente participa da operação. Registre qual parte ficou real e qual parte continua controlada, porque os dois testes respondem perguntas diferentes.

Essa separação também evita uma falsa sensação de cobertura. O artigo acadêmico An Empirical Study of Flaky Tests in JavaScript, consultado em 29/09/2026, estuda flakiness em projetos JavaScript. Ele é uma evidência sobre o problema de confiabilidade dos testes, não uma prova de que esta fixture resolve todas as corridas.

Perguntas frequentes

Preciso executar o teste mil vezes?

Não para provar a ordem que você já consegue controlar. Libere as Promises na sequência suspeita e execute uma asserção determinística. Repetições podem complementar a suíte para cenários de ambiente, mas não devem ser o único caminho para alcançar uma intercalação específica.

Uma condição de corrida só existe com várias threads?

Não. Duas operações assíncronas podem alternar em pontos de espera e produzir um estado incorreto mesmo quando o código JavaScript roda em uma única thread. O teste precisa representar a alternância que importa, seja entre Promises, callbacks, requisições, mensagens ou transações.

Devo cancelar a requisição antiga ou ignorar sua resposta?

Depende do contrato. Cancelar economiza trabalho quando a dependência respeita o sinal, mas não desfaz necessariamente um efeito que já começou. Ignorar a resposta protege o estado local. Para decidir, teste também o efeito remoto e documente o que o cancelamento realmente significa.

Conclusão

Uma condição de corrida deixa de ser um acidente de timing quando o teste consegue escolher a ordem em que as operações terminam. Crie Promises adiadas, force a sequência que ameaça a invariável e faça o caso falhar quando o guard desaparece.

Depois, leve a mesma afirmação para a fronteira real. Banco, fila, navegador e serviço remoto podem acrescentar regras que uma fixture em memória não vê. O resultado é uma suíte com duas provas honestas: uma explica a lógica e outra verifica o ambiente que pode quebrá-la.

Fontes consultadas