Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Test Sharding no Node.js: acelere o CI

Atualizado em: 9 de outubro de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

Test Sharding no Node.js divide uma suíte grande em partes independentes para executá-las em paralelo em vários runners de CI. Em vez de um único job rodar todos os testes por vinte minutos, quatro shards podem processar subconjuntos diferentes e reduzir o tempo total, desde que a distribuição seja equilibrada e os testes não compartilhem estado.

O Node.js Test Runner executa arquivos de teste em processos isolados por padrão e permite controlar concorrência. Mesmo quando a versão usada não oferece uma flag específica de shard, é possível distribuir arquivos de forma determinística com globs, listas geradas ou hashing do caminho. O ponto central é garantir que cada arquivo pertença a exatamente um shard e que a união de todos cubra a suíte inteira.

Neste guia, você aprenderá a criar shards determinísticos, integrar com GitHub Actions, balancear por duração, lidar com cobertura, bancos, retries, flakiness e relatórios.

Quando usar sharding?

Sharding faz sentido quando:

  • a suíte já está confiável;
  • o tempo de CI é alto;
  • há vários runners disponíveis;
  • os testes podem usar recursos isolados;
  • o custo adicional de preparar ambientes é menor que o ganho;
  • os relatórios podem ser agregados.

Não comece por sharding quando testes falham por ordem, compartilham banco sem isolamento ou dependem de portas fixas. Dividir uma suíte instável apenas multiplica falhas difíceis de reproduzir.

Concorrência não é sharding

O Test Runner pode executar vários arquivos simultaneamente no mesmo job:

node --test --test-concurrency=4

Isso usa os recursos da mesma máquina. Sharding distribui arquivos entre jobs ou máquinas diferentes. As duas técnicas podem ser combinadas, mas aumentar ambas sem limite causa contenção de CPU, banco, disco e rede.

Estratégia determinística por índice

Liste os arquivos em ordem estável e selecione aqueles cujo índice pertence ao shard:

// scripts/select-test-shard.mjs
import { glob } from 'node:fs/promises';

const shardIndex = Number(process.env.SHARD_INDEX);
const shardTotal = Number(process.env.SHARD_TOTAL);

if (!Number.isInteger(shardIndex) || shardIndex < 0) {
  throw new Error('SHARD_INDEX inválido');
}

if (!Number.isInteger(shardTotal) || shardTotal < 1) {
  throw new Error('SHARD_TOTAL inválido');
}

const files = [];
for await (const file of glob('test/**/*.test.js')) {
  files.push(file);
}

files.sort();

const selected = files.filter((_, index) => {
  return index % shardTotal === shardIndex;
});

process.stdout.write(selected.join('\n'));

Esse algoritmo garante que cada arquivo aparece uma vez. Porém, pode ficar desequilibrado se alguns arquivos duram muito mais que outros.

Executando os arquivos selecionados

FILES=$(SHARD_INDEX=0 SHARD_TOTAL=4 node scripts/select-test-shard.mjs)
node --test $FILES

Em projetos com nomes contendo espaços, evite expansão simples do shell. Gere JSON e use um script Node.js para chamar o processo com argumentos separados:

import { spawn } from 'node:child_process';

const child = spawn(process.execPath, ['--test', ...selected], {
  stdio: 'inherit'
});

child.once('exit', code => {
  process.exitCode = code ?? 1;
});

Consulte child_process no Node.js para evitar injeção e tratar sinais.

Hash do caminho

Quando arquivos são adicionados, o método por índice pode mover muitos testes entre shards. Um hash estável reduz mudanças:

import { createHash } from 'node:crypto';

function shardFor(file, total) {
  const digest = createHash('sha256').update(file).digest();
  return digest.readUInt32BE(0) % total;
}

const selected = files.filter(file => {
  return shardFor(file, shardTotal) === shardIndex;
});

O resultado é estável para o mesmo total de shards. Se o total muda, a distribuição também muda.

Balanceamento por duração

O melhor equilíbrio usa o histórico de tempo. Ordene arquivos do mais lento para o mais rápido e sempre atribua o próximo ao shard com menor soma:

const shards = Array.from({ length: shardTotal }, () => ({
  durationMs: 0,
  files: []
}));

for (const testFile of filesByDurationDesc) {
  shards.sort((a, b) => a.durationMs - b.durationMs);
  shards[0].files.push(testFile.path);
  shards[0].durationMs += testFile.durationMs;
}

Armazene durações como artifact do CI ou em um serviço próprio. Use média móvel para evitar que um único runner lento distorça o histórico.

Medição de duração

Um reporter customizado pode registrar eventos de início e conclusão. Outra opção é executar arquivos individualmente em uma etapa periódica. Não adicione um processo por arquivo em toda execução se isso aumentar muito o tempo.

Para métricas de tempo dentro do processo, veja perf_hooks no Node.js.

GitHub Actions

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        shard: [0, 1, 2, 3]

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci

      - name: Run shard
        env:
          SHARD_INDEX: ${{ matrix.shard }}
          SHARD_TOTAL: 4
        run: node scripts/run-test-shard.mjs

fail-fast: false deixa todos os shards terminarem, produzindo diagnóstico completo. Veja CI para Node.js com GitHub Actions.

Runners e recursos

Quatro shards não garantem uma execução quatro vezes mais rápida. Cada job repete checkout, instalação, build e preparação de serviços. Meça:

  • tempo de fila;
  • tempo de instalação;
  • tempo de setup;
  • tempo real dos testes;
  • custo de runners;
  • contenção de dependências externas.

Às vezes, dois shards com concorrência interna são mais eficientes que oito jobs pequenos.

Isolamento de banco

Cada shard precisa de banco próprio ou namespace isolado:

DATABASE_URL=postgresql://test:test@localhost/app_test_${SHARD_INDEX}

Alternativas:

  • schema PostgreSQL por shard;
  • database por shard;
  • container por job;
  • prefixo exclusivo em Redis;
  • bucket temporário por execução.

Para ambientes descartáveis, consulte Testcontainers no Node.js.

Portas dinâmicas

Não use uma porta fixa como 3000 em vários testes. Escute em porta zero:

await server.listen({ port: 0, host: '127.0.0.1' });
const address = server.address();

O sistema operacional escolhe uma porta livre. Feche o servidor em after ou afterEach.

Arquivos temporários

Crie diretório por teste ou shard:

const directory = await mkdtemp(
  path.join(os.tmpdir(), `app-${process.env.SHARD_INDEX}-`)
);

Remova no teardown e não compartilhe nomes estáticos.

Global setup

O Test Runner suporta módulo de setup global em versões atuais. Em sharding, esse setup roda em cada job. Use-o para preparar apenas recursos daquele shard, não para alterar infraestrutura compartilhada sem coordenação.

Testes dependentes de ordem

Sharding expõe dependências ocultas. Cada arquivo deve criar e remover seu próprio estado. Não presuma que outro teste criou usuário, tabela, fixture ou variável global.

Execute periodicamente com ordem aleatória quando suportado:

node --test --test-randomize

Guarde a seed de falha para reprodução.

Flaky tests

Não use retry automático para esconder instabilidade. Quando houver retry:

  • registre a primeira falha;
  • marque o teste como flaky;
  • abra tarefa de correção;
  • limite tentativas;
  • não altere o resultado de cobertura silenciosamente.

Rerun de falhas

Versões atuais oferecem persistência para reexecutar somente testes que ainda não passaram:

node --test \
  --test-rerun-failures=.cache/test-rerun.json

O arquivo precisa permanecer específico por shard, pois caminhos e tentativas de jobs diferentes não devem ser misturados.

Cobertura distribuída

Cada shard gera dados parciais. Para obter cobertura global:

  1. execute cobertura em cada shard;
  2. salve os arquivos brutos ou LCOV;
  3. baixe todos em um job agregador;
  4. mescle antes de aplicar thresholds.

Não aplique 80% em cada shard isolado, porque cada um enxerga apenas parte do código. A regra deve considerar a união.

Module Compile Cache e cobertura

Desative compile cache em jobs de cobertura, pois o code cache pode reduzir precisão em funções restauradas:

NODE_DISABLE_COMPILE_CACHE=1 node --test --experimental-test-coverage

Consulte Module Compile Cache no Node.js.

Relatórios JUnit

Gere um arquivo por shard:

test-results/shard-0.xml
test-results/shard-1.xml

O job final agrega ou publica todos. Inclua o shard no nome para evitar sobrescrita.

Snapshots

Atualização de snapshots não deve ocorrer em vários shards simultaneamente contra o mesmo workspace. Execute --test-update-snapshots em um job dedicado e revise o diff. Veja Snapshot Tests no Node.js.

Testes por tags

Quando a versão do Node.js suporta tags, elas podem separar integração, banco e testes lentos. Tags não substituem sharding de arquivos, mas ajudam a criar pipelines diferentes:

describe('database', { tags: ['db'] }, () => {
  it('insere pedido', { tags: ['integration'] }, async () => {});
});

Shards vazios

Quando o total de shards supera a quantidade de arquivos, alguns ficam vazios. O script deve terminar com sucesso e registrar que não havia testes, ou reduzir a matrix dinamicamente.

Validação da distribuição

Antes de executar, verifique:

const assigned = shards.flatMap(shard => shard.files);

if (new Set(assigned).size !== files.length) {
  throw new Error('Arquivo duplicado ou ausente na divisão');
}

Também compare a lista ordenada original com a união dos shards.

Falha de um shard

O workflow completo deve falhar quando qualquer shard falha. Não use continue-on-error no job principal. Um job agregador pode usar if: always() para publicar relatórios, mas deve preservar o resultado final.

Cancelamento

Configure concurrency do workflow para cancelar execuções antigas da mesma branch. Quando um shard recebe SIGTERM, feche banco, servidores e containers para não deixar recursos órfãos.

Segurança

Shards de pull requests não confiáveis não devem receber segredos. Use serviços locais e credenciais temporárias. Não construa nomes SQL diretamente com um índice sem validar; o índice deve ser inteiro e limitado.

Erros comuns

  • dividir testes instáveis;
  • compartilhar o mesmo banco;
  • usar portas fixas;
  • aplicar cobertura por shard;
  • não agregar relatórios;
  • usar mais shards do que o necessário;
  • não considerar tempo de setup;
  • mover arquivos entre shards sem histórico de duração;
  • ocultar falhas com retries ilimitados.

Fluxo recomendado

  1. estabilize a suíte;
  2. meça os arquivos mais lentos;
  3. comece com dois shards;
  4. isole banco, portas e arquivos;
  5. agregue cobertura e relatórios;
  6. meça tempo total e custo;
  7. balanceie por histórico;
  8. aumente apenas quando houver ganho.

Conclusão

Test Sharding no Node.js reduz o tempo de feedback ao distribuir arquivos entre runners independentes. Uma divisão determinística por índice ou hash é simples; uma distribuição baseada em duração melhora o equilíbrio em suítes grandes.

O ganho depende de isolamento e observabilidade. Use bancos e diretórios por shard, portas dinâmicas, cobertura agregada e relatórios com nomes exclusivos. Assim, paralelismo acelera o CI sem transformar testes em uma fonte de concorrência e flakiness.

Consulte a documentação oficial do Node.js Test Runner e a documentação de matrix do GitHub Actions.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita