Usar Testcontainers no Node.js permite executar testes de integração contra PostgreSQL, MongoDB, Redis, Kafka, RabbitMQ, Elasticsearch e outros serviços reais iniciados em containers descartáveis. Em vez de simular o comportamento de uma dependência complexa, o teste sobe uma instância isolada, aplica configuração e a remove ao terminar.
Essa abordagem detecta problemas que mocks não reproduzem: SQL inválido, índices ausentes, diferenças de versão, transações, encoding, timeouts, autenticação e comportamento do protocolo. O custo é um ambiente de teste mais pesado, que precisa de runtime de containers, estratégias de espera e limpeza confiável.
Neste guia, você aprenderá instalação, GenericContainer, módulos, wait strategies, redes, fixtures, migrations, paralelismo, CI, reutilização, logs, segurança e boas práticas.
O que é Testcontainers?
Testcontainers é uma biblioteca para iniciar dependências reais durante testes. A documentação oficial do Testcontainers para Node.js apresenta containers genéricos, imagens, networking, Docker Compose, wait strategies e módulos para bancos e brokers. A documentação do projeto também descreve runtimes de container suportados e configuração.
Para estruturar testes nativos, consulte Node Test Runner. Para ambientes de banco versionados, veja Migrações de Banco no Node.js.
Instalação
npm install --save-dev testcontainersMódulos específicos podem ser instalados separadamente, conforme a versão atual da biblioteca.
Requisitos
O ambiente precisa de Docker ou outro runtime compatível. O usuário do CI deve conseguir criar containers, redes e volumes.
Primeiro container
import { GenericContainer } from 'testcontainers';
const container = await new GenericContainer('redis:8-alpine')
.withExposedPorts(6379)
.start();
const host = container.getHost();
const port = container.getMappedPort(6379);
// executar testes
await container.stop();O host e a porta mapeada não devem ser presumidos. Testcontainers escolhe recursos livres.
Cleanup com try/finally
const container = await startDependency();
try {
await runTests(container);
} finally {
await container.stop();
}Mesmo quando o teste falha, o container precisa ser removido.
Hooks do runner
let postgres;
before(async () => {
postgres = await startPostgres();
});
after(async () => {
await postgres.stop();
});Use o hook global do arquivo ou suíte para reduzir tempo de startup.
PostgreSQL module
import { PostgreSqlContainer } from '@testcontainers/postgresql';
const postgres = await new PostgreSqlContainer(
'postgres:18-alpine'
)
.withDatabase('app_test')
.withUsername('test')
.withPassword('test')
.start();
const connectionString = postgres.getConnectionUri();Fixe uma versão de imagem compatível com produção, não use latest.
Aplicando migrations
await migrate({
databaseUrl: postgres.getConnectionUri()
});Os testes devem criar o schema da mesma forma que produção. Isso verifica que todas as migrations funcionam em banco vazio.
Seeds
Insira apenas dados necessários para cada suíte:
await seedCustomer(db, {
id: 'customer-1',
plan: 'premium'
});Fixtures pequenas tornam falhas compreensíveis.
Banco limpo por teste
Opções:
- transaction rollback;
- truncate entre testes;
- schema separado;
- database separado;
- container por suíte;
- container por teste para isolamento máximo.
Escolha equilíbrio entre velocidade e independência.
Rollback de transação
Funciona quando o código usa a mesma conexão e não testa commits reais. Para filas, conexões múltiplas e transações independentes, truncate ou banco separado é mais confiável.
Container por suíte
Subir uma instância e limpar dados entre casos costuma ser eficiente. Se testes alteram configuração global, use instâncias separadas.
Wait strategies
Container iniciado não significa serviço pronto. Use estratégia de espera:
import { Wait } from 'testcontainers';
const container = await new GenericContainer(image)
.withExposedPorts(8080)
.withWaitStrategy(
Wait.forHttp('/health', 8080)
.forStatusCode(200)
)
.start();Logs, portas e health endpoints também podem indicar readiness.
Evite sleep fixo
await new Promise(resolve => setTimeout(resolve, 5000));Esse padrão deixa testes lentos e flakey. Espere uma condição real.
Timeout de startup
.withStartupTimeout(60_000)Ajuste para imagem e ambiente do CI. Falhe com logs úteis quando exceder.
Variáveis de ambiente
const api = await new GenericContainer('my-api:test')
.withEnvironment({
DATABASE_URL: postgres.getConnectionUri(),
NODE_ENV: 'test'
});Não use credenciais de produção.
Arquivos e configuração
É possível copiar conteúdo ou montar arquivos, conforme o runtime. Prefira imagens reproduzíveis e configuração pequena.
Networking
import { Network } from 'testcontainers';
const network = await new Network().start();
const database = await new GenericContainer('postgres:18-alpine')
.withNetwork(network)
.withNetworkAliases('database')
.start();
const api = await new GenericContainer('my-api:test')
.withNetwork(network)
.withEnvironment({
DATABASE_HOST: 'database'
})
.start();Containers na mesma rede usam aliases, não portas do host.
Host versus rede
O processo de teste no host usa getHost() e getMappedPort(). Outro container usa alias e porta interna.
Docker Compose
Quando a aplicação depende de vários serviços definidos em Compose, Testcontainers pode iniciar o conjunto. Ainda assim, controle imagens, healthchecks e limpeza.
Redis
import { RedisContainer } from '@testcontainers/redis';
const redis = await new RedisContainer(
'redis:8-alpine'
).start();Teste scripts Lua, TTL, Streams e comportamento real. Consulte Redis Streams no Node.js.
MongoDB
Use o módulo MongoDB e uma topologia compatível quando testar transactions ou change streams. Uma instância standalone não reproduz replica set.
Consulte MongoDB no Node.js.
Kafka e RabbitMQ
Brokers precisam de readiness e configuração corretas. Teste publish, consumo, redelivery, idempotência e dead-letter, não apenas conexão.
Elasticsearch
Subir Elasticsearch pode exigir mais memória. Defina limites e use imagem compatível com o cliente.
Consulte Elasticsearch no Node.js.
ToxiProxy
O módulo ToxiProxy permite injetar latência, desconexão e perda de conexão para testar resiliência.
await proxy.setConnectionCut(true);
A API exata depende da versão do módulo.
Testando timeout
Adicione latência acima do limite do cliente e confirme:
- erro classificado;
- cancelamento;
- retry limitado;
- circuit breaker;
- liberação de recursos.
Testando reconexão
Pare e reinicie a dependência ou corte a rede. Confirme que o cliente volta sem multiplicar listeners ou perder estado indevidamente.
Imagens customizadas
Você pode construir uma imagem para testes:
import { GenericContainer, ImageFromDockerfile } from 'testcontainers';
const image = await ImageFromDockerfile()
.withDockerfileFromBuilder(builder)
.build();Evite builds pesados em cada suíte. Use cache do CI.
Imagens privadas
O ambiente precisa de autenticação no registry. Nunca escreva token em logs.
Logs do container
const stream = await container.logs();
stream.on('data', line => {
logger.debug({
container: 'database',
line: line.toString()
});
});Anexe logs apenas em falha para reduzir ruído.
Paralelismo
Testes paralelos podem disputar:
- CPU;
- memória;
- pull de imagens;
- conexões;
- nomes de resources;
- limites do runner.
Use portas dinâmicas e IDs únicos.
Limite de concorrência
Não execute cinquenta clusters pesados no mesmo runner. Separe suítes e configure workers de acordo com recursos.
Reutilização
Algumas configurações permitem reutilizar containers localmente. Isso acelera desenvolvimento, mas pode preservar estado. Em CI, prefira isolamento e limpeza previsível.
Global setup
Um setup global pode iniciar dependências uma vez e compartilhar dados de conexão. Garanta que o teardown execute e que testes não dependam de ordem.
CI com Docker
O runner precisa acessar o daemon. Em ambientes containerizados, opções incluem socket montado, Docker-in-Docker ou serviço remoto. Cada abordagem possui implicações de segurança.
Segurança do daemon
Acesso ao Docker socket equivale a privilégios elevados no host. Use runners isolados, efêmeros e controlados.
Pull de imagens
Faça cache ou pre-pull de imagens frequentes. Fixe tags e, para maior reprodutibilidade, digests.
Multi-arch
Desenvolvedores ARM e CI x86 podem usar imagens diferentes. Escolha imagens multi-arch ou configure plataforma conscientemente.
Testes de repositories
Execute a mesma suíte de contrato contra o adapter real:
- salvar e recuperar;
- constraints;
- transações;
- paginação;
- locks;
- tipos;
- migrations.
Consulte Repository Pattern no Node.js.
Testes de integração HTTP
Suba apenas dependências externas e execute a aplicação no processo do teste. Isso facilita coverage e debugging.
Teste end-to-end em container
Para validar imagem final, suba API, banco e broker em rede comum. Execute requests pelo host e confirme healthcheck e shutdown.
Fixtures determinísticas
Use datas e IDs fixos. Não dependa de dados previamente existentes.
Relógio
Para TTL e tarefas temporais, injete Clock quando possível. Testcontainers não torna o relógio do sistema automaticamente controlável.
Performance dos testes
Meça:
- tempo de pull;
- startup;
- migrations;
- execução;
- cleanup.
Otimize o maior componente, não suponha que o container é o gargalo.
Separando níveis
- unitários: rápidos e sem container;
- integração: adapter com dependência real;
- end-to-end: aplicação e infraestrutura;
- resiliência: falhas e rede;
- carga: ferramentas específicas.
Não transforme todo teste em end-to-end.
Observabilidade em falha
Quando uma suíte falhar, preserve:
- logs do container;
- versão da imagem;
- porta e rede;
- resultado de healthcheck;
- migrations aplicadas;
- tempo de startup.
Teste flakey
Não repita automaticamente até passar sem investigar. Procure race condition, readiness incorreta, isolamento insuficiente e recursos saturados.
Erros comuns
- Usar latest: resultados mudam sem alteração no código.
- Sleep fixo: testes ficam lentos e instáveis.
- Porta fixa: paralelismo quebra.
- Sem teardown: recursos acumulam.
- Estado compartilhado: testes dependem de ordem.
- Mockar o adapter real: bugs de integração passam.
- CI sem recursos: containers falham aleatoriamente.
- Socket exposto: risco de segurança aumenta.
Boas práticas
- Fixe versões de imagens.
- Use wait strategies.
- Mapeie portas dinamicamente.
- Aplique migrations reais.
- Limpe dados entre testes.
- Limite paralelismo.
- Capture logs em falha.
- Use runners isolados.
- Teste resiliência.
- Mantenha unitários rápidos.
Conclusão
Usar Testcontainers no Node.js aproxima os testes do comportamento real de bancos, brokers e serviços. Containers descartáveis revelam problemas de protocolo, configuração, transação e versão que mocks não cobrem.
Com imagens fixas, wait strategies, limpeza, migrations e CI dimensionado, a suíte permanece reproduzível. Testcontainers deve complementar testes unitários, concentrando-se nas fronteiras em que infraestrutura real muda o resultado.



