Testcontainers permite criar dependências reais e descartáveis durante testes Node.js. Em vez de simular PostgreSQL, Redis, Kafka ou outro serviço com mocks, a suíte inicia containers Docker, aguarda ficarem prontos, executa testes e remove os recursos no final.
Essa abordagem aumenta a fidelidade dos testes de integração. Consultas SQL, migrations, índices, autenticação, protocolos e comportamento de rede são exercitados contra o software real, em uma versão controlada pela imagem.
Quando usar
Testcontainers é indicado para testar:
- repositórios e migrations;
- filas e brokers;
- cache Redis;
- armazenamento S3 compatível;
- Elasticsearch ou OpenSearch;
- serviços locais simulados como LocalStack;
- integrações entre vários componentes;
- falhas de rede com ToxiProxy.
Testes unitários rápidos continuam importantes. Use containers onde o comportamento externo é parte do risco.
Requisitos
O ambiente precisa de um runtime compatível, normalmente Docker. Valide:
docker version
docker run --rm hello-worldEm CI, o runner deve permitir containers e acesso ao daemon. Restrições de segurança, Docker rootless e ambientes remotos podem exigir configuração.
Instalação
npm install -D testcontainers
Para PostgreSQL, é possível usar módulo específico:
npm install -D @testcontainers/postgresql pgPrimeiro container genérico
import { GenericContainer } from 'testcontainers';
const container = await new GenericContainer('redis:8-alpine')
.withExposedPorts(6379)
.start();
const host = container.getHost();
const port = container.getMappedPort(6379);
console.log({ host, port });
await container.stop();Não assuma que a porta externa será 6379. Testcontainers escolhe uma porta livre e fornece o mapeamento.
PostgreSQL com módulo
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import pg from 'pg';
const postgres = await new PostgreSqlContainer('postgres:18-alpine')
.withDatabase('app_test')
.withUsername('app')
.withPassword('senha_teste')
.start();
const pool = new pg.Pool({
connectionString: postgres.getConnectionUri(),
});
await pool.query('select 1');
await pool.end();
await postgres.stop();O módulo encapsula variáveis, portas e URI de conexão.
Integração com Node Test Runner
import { before, after, test } from 'node:test';
import assert from 'node:assert/strict';
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import pg from 'pg';
let container;
let pool;
before(async () => {
container = await new PostgreSqlContainer('postgres:18-alpine').start();
pool = new pg.Pool({ connectionString: container.getConnectionUri() });
await pool.query('create table users(id serial primary key, name text not null)');
});
after(async () => {
await pool?.end();
await container?.stop();
});
test('grava e consulta usuário', async () => {
const inserted = await pool.query(
'insert into users(name) values($1) returning id, name',
['Ana'],
);
assert.equal(inserted.rows[0].name, 'Ana');
});Setup global ou por arquivo
Iniciar um container por teste pode ser lento. Estratégias:
- um container por arquivo de teste;
- um container por suíte;
- container compartilhado com banco limpo entre testes;
- container isolado por worker em execução paralela.
Compartilhar reduz tempo, mas aumenta risco de vazamento de estado. Escolha isolamento primeiro e otimize depois de medir.
Migrations reais
Execute a mesma ferramenta usada em produção:
before(async () => {
container = await new PostgreSqlContainer('postgres:18-alpine').start();
process.env.DATABASE_URL = container.getConnectionUri();
await executarMigrations();
});Isso detecta migrations inválidas, ordem incorreta, extensões ausentes e diferenças de versão.
Wait strategies
Uma porta aberta não significa que o serviço está pronto. Use estratégia adequada:
import { GenericContainer, Wait } from 'testcontainers';
const container = await new GenericContainer('minio/minio')
.withCommand(['server', '/data'])
.withExposedPorts(9000)
.withWaitStrategy(Wait.forLogMessage(/API:/))
.start();Também existem espera por healthcheck, HTTP, logs e outras condições. Prefira o sinal oficial de prontidão do serviço.
Variáveis de ambiente e comandos
const container = await new GenericContainer('minio/minio')
.withEnvironment({
MINIO_ROOT_USER: 'teste',
MINIO_ROOT_PASSWORD: 'senha-segura-de-teste',
})
.withCommand(['server', '/data'])
.withExposedPorts(9000)
.start();Credenciais de teste devem ser exclusivas do ambiente descartável.
Arquivos e inicialização
É possível copiar arquivos:
const container = await new GenericContainer('postgres:18-alpine')
.withCopyFilesToContainer([
{
source: './test/fixtures/init.sql',
target: '/docker-entrypoint-initdb.d/init.sql',
},
])
.start();Para cenários complexos, preferir migrations programáticas pode produzir erros mais claros.
Redes entre containers
Crie uma rede para serviços se comunicarem por alias:
import { Network, GenericContainer } from 'testcontainers';
const network = await new Network().start();
const redis = await new GenericContainer('redis:8-alpine')
.withNetwork(network)
.withNetworkAliases('redis')
.withExposedPorts(6379)
.start();
const app = await new GenericContainer('minha-app:test')
.withNetwork(network)
.withEnvironment({ REDIS_URL: 'redis://redis:6379' })
.start();Da máquina de testes, use host e porta mapeada. Entre containers, use alias e porta interna.
Docker Compose
Testcontainers pode iniciar um arquivo Compose existente. Isso é útil para cenários que já descrevem vários serviços, mas um teste programático costuma oferecer controle mais fino e melhor isolamento.
Imagens fixas
Evite tags flutuantes como latest. Use:
postgres:18.1-alpine
redis:8.2-alpineFixar digest oferece ainda mais reprodutibilidade. Atualize em pull request dedicado.
Logs de container
Em falhas, capture logs:
const stream = await container.logs();
stream
.on('data', (line) => console.log(line.toString()))
.on('err', (line) => console.error(line.toString()));Evite imprimir segredos. No CI, preserve logs como artefato somente quando necessário.
Timeouts
Primeiro download pode levar mais tempo. Ajuste timeout da suíte sem esconder travamentos. Separe timeout de inicialização, consulta e encerramento.
Reutilização de containers
Reuso pode acelerar desenvolvimento local, mas reduz isolamento e pode deixar estado antigo. Em CI, prefira containers descartáveis. Se habilitar reuso local, documente como limpar.
Execução paralela
Portas aleatórias permitem paralelismo, mas banco e nomes de recursos precisam ser únicos. Não use uma variável global de conexão compartilhada por workers diferentes.
CI com GitHub Actions
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm ci
- run: npm testRunners hospedados normalmente possuem Docker. Em runners próprios, valide permissões e espaço em disco.
Limpeza e Ryuk
Testcontainers usa mecanismos para remover recursos, inclusive quando testes falham. Não desative o reaper sem entender o impacto. Processos mortos abruptamente podem deixar containers; configure limpeza periódica no runner.
Testes de falha de rede
Com ToxiProxy, simule latência, timeout, perda de conexão e largura de banda. Isso ajuda a validar retries, circuit breakers e idempotência sem alterar o serviço real.
Testcontainers Cloud
Ambientes sem Docker local podem usar runtime remoto compatível. Considere latência, segurança, acesso à rede e custo.
Segurança
Acesso ao daemon Docker é altamente privilegiado. Não execute código não confiável com acesso irrestrito. Pull requests externos precisam de isolamento e ausência de segredos.
Testcontainers ou mocks
Mocks são rápidos e bons para lógica local. Containers validam integração real. Uma pirâmide equilibrada usa muitos testes unitários, alguns testes com containers e poucos testes de ponta a ponta.
Fluxo recomendado
Fixe imagens, use wait strategy real, execute migrations, isole dados, encerre clientes antes dos containers e preserve logs de falhas. Combine com Node Test Runner, transações em PostgreSQL no Node.js, filas em processamento de jobs e pipelines de GitHub Actions.
Consulte a documentação oficial do Testcontainers para Node.js e o repositório oficial testcontainers-node.



